From 2a950bc61f66d4f43e9aa8f19c5798f279ec46cd Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sat, 27 Feb 2021 22:26:58 +0100 Subject: [PATCH 01/27] screen readers / text selection / translation V0 --- .../src/facade/SelectionManagerFacade.js | 22 +- .../troika-3d-text/src/facade/Text3DFacade.js | 4 +- packages/troika-examples/text/TextExample.jsx | 7 +- packages/troika-three-text/src/Text.js | 357 +++++++++++++++--- 4 files changed, 338 insertions(+), 52 deletions(-) diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index 2b8b4224..31e0c969 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -1,6 +1,5 @@ import { ListFacade } from 'troika-3d' import { Matrix4, Plane, Vector2, Vector3 } from 'three' -import { getCaretAtPoint, getSelectionRects } from 'troika-three-text' import { invertMatrix4 } from 'troika-three-utils' import SelectionRangeRect from './SelectionRangeRect.js' @@ -42,10 +41,13 @@ class SelectionManagerFacade extends ListFacade { } const onDragStart = e => { + if(e.which===3){//contextmenu + return false + } const textRenderInfo = textMesh.textRenderInfo if (textRenderInfo) { const textPos = textMesh.worldPositionToTextCoords(e.intersection.point, tempVec2) - const caret = getCaretAtPoint(textRenderInfo, textPos.x, textPos.y) + const caret = textMesh.getCaret(textPos.x, textPos.y) if (caret) { onSelectionChange(caret.charIndex, caret.charIndex) parent.addEventListener('drag', onDrag) @@ -56,6 +58,9 @@ class SelectionManagerFacade extends ListFacade { } const onDrag = e => { + if(e.which===3){//contextmenu + return false + } const textRenderInfo = textMesh.textRenderInfo if (e.ray && textRenderInfo) { // If it's hitting on the Text mesh, do an exact translation; otherwise raycast to an @@ -69,7 +74,7 @@ class SelectionManagerFacade extends ListFacade { textPos = ray.intersectPlane(tempPlane.setComponents(0, 0, 1, 0), tempVec3) } if (textPos) { - const caret = getCaretAtPoint(textRenderInfo, textPos.x, textPos.y) + const caret = textMesh.testCaret(textPos.x, textPos.y) if (caret) { onSelectionChange(this.selectionStart, caret.charIndex) } @@ -85,6 +90,16 @@ class SelectionManagerFacade extends ListFacade { parent.addEventListener('dragstart', onDragStart) parent.addEventListener('mousedown', onDragStart) + var canvas = parent.getSceneFacade().parent._threeRenderer.domElement; + canvas.addEventListener('contextmenu',(e)=>{ + textMesh._domElSelectedText.style.pointerEvents = 'auto' + textMesh.updateSelection() + window.setTimeout(()=>{ + textMesh._domElSelectedText.style.pointerEvents = 'none' + console.log('contextmenu') + },50) + console.log('contextmenu') + }) this._cleanupEvents = () => { onDragEnd() @@ -94,7 +109,6 @@ class SelectionManagerFacade extends ListFacade { } afterUpdate() { - this.data = getSelectionRects(this.textRenderInfo, this.selectionStart, this.selectionEnd) super.afterUpdate() } diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index 4b259d08..d977acb9 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -35,7 +35,9 @@ const TEXT_MESH_PROPS = [ 'orientation', 'glyphGeometryDetail', 'sdfGlyphSize', - 'debugSDF' + 'debugSDF', + 'supportScreenReader', + 'selectable' ] diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index f0652910..d46ec05a 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -121,10 +121,11 @@ class TextExample extends React.Component { material: 'MeshStandardMaterial', useTexture: false, shadows: false, - selectable: false, + selectable: true, colorRanges: false, sdfGlyphSize: 6, - debugSDF: false + debugSDF: false, + supportScreenReader: true } this._onConfigUpdate = (newState) => { @@ -212,6 +213,7 @@ class TextExample extends React.Component { strokeWidth: state.strokeWidth, strokeColor: state.strokeColor, curveRadius: state.curveRadius, + supportScreenReader: state.supportScreenReader, material: material, color: 0xffffff, scaleX: state.textScale || 1, @@ -314,6 +316,7 @@ class TextExample extends React.Component { {type: 'boolean', path: "shadows", label: "Shadows"}, {type: 'boolean', path: "colorRanges", label: "colorRanges (WIP)"}, {type: 'boolean', path: "selectable", label: "Selectable (WIP)"}, + {type: 'boolean', path: "supportScreenReader", label: "Support Screen readers (WIP)"}, {type: 'number', path: "fontSize", label: "fontSize", min: 0.01, max: 0.2, step: 0.01}, {type: 'number', path: "textScale", label: "scale", min: 0.1, max: 10, step: 0.1}, //{type: 'number', path: "textIndent", label: "indent", min: 0.1, max: 1, step: 0.01}, diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 237c09e4..a1cbb529 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -5,12 +5,17 @@ import { Mesh, MeshBasicMaterial, PlaneBufferGeometry, + Vector4, Vector3, Vector2, + BoxBufferGeometry } from 'three' import { GlyphsGeometry } from './GlyphsGeometry.js' import { createTextDerivedMaterial } from './TextDerivedMaterial.js' import { getTextRenderInfo } from './TextBuilder.js' +import { createDerivedMaterial } from 'troika-three-utils' +import { getSelectionRects, getCaretAtPoint } from './selectionUtils' + const Text = /*#__PURE__*/(() => { @@ -32,22 +37,10 @@ const Text = /*#__PURE__*/(() => { return Array.isArray(o) ? o[0] : o } - let getFlatRaycastMesh = () => { - const mesh = new Mesh( - new PlaneBufferGeometry(1, 1), - defaultMaterial - ) - getFlatRaycastMesh = () => mesh - return mesh - } - let getCurvedRaycastMesh = () => { - const mesh = new Mesh( - new PlaneBufferGeometry(1, 1, 32, 1), - defaultMaterial - ) - getCurvedRaycastMesh = () => mesh - return mesh - } + const raycastMesh = new Mesh( + new PlaneBufferGeometry(1, 1).translate(0.5, 0.5, 0), + defaultMaterial + ) const syncStartEvent = {type: 'syncstart'} const syncCompleteEvent = {type: 'synccomplete'} @@ -74,7 +67,6 @@ const Text = /*#__PURE__*/(() => { 'color', 'depthOffset', 'clipRect', - 'curveRadius', 'orientation', 'glyphGeometryDetail' ) @@ -89,6 +81,37 @@ const Text = /*#__PURE__*/(() => { */ class Text extends Mesh { constructor() { + + this._domElSelectedText = document.createElement('p') + this._domElText = document.createElement(this.tagName ? this.tagName : 'p') + this.selectionStartIndex = 0; + this.selectionEndIndex = 0; + this.selectedText = null; + + if(this.domContainer){ + this.domContainer.appendChild(this._domElSelectedText) + this.domContainer.appendChild(this._domElText) + }else{ + document.body.appendChild(this._domElSelectedText) + document.body.appendChild(this._domElText) + } + + this._domElSelectedText.setAttribute('aria-hidden','true') + this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' + this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' + + this.startObservingMutation() + + this.selectionRect = [] + + //TODO test html support + + //syncing html on top of text can slow down the page if used with multiple Text instance + //sometime the text is purely decorative and it makes no sense for it to be accessible, so it should be possible to disable / enable it + //the default is to be discussed + this.supportScreenReader = false + this.selectable = false + const geometry = new GlyphsGeometry() super(geometry, null) @@ -99,6 +122,8 @@ const Text = /*#__PURE__*/(() => { * The string of text to be rendered. */ this.text = '' + this.prevText = '' + this.currentText = '' /** * @deprecated Use `anchorX` and `anchorY` instead @@ -384,6 +409,15 @@ const Text = /*#__PURE__*/(() => { if (this._needsSync) { this._needsSync = false + /* detect text change coming from the component */ + if(this.prevText !== this.text){ + this.currentText = this.text + this.selectionStartIndex = this.selectionEndIndex = -1 + this.prevText = this.text + } + + this.currentText = this.currentText ? this.currentText : this.text + // If there's another sync still in progress, queue if (this._isSyncing) { (this._queuedSyncs || (this._queuedSyncs = [])).push(callback) @@ -392,7 +426,7 @@ const Text = /*#__PURE__*/(() => { this.dispatchEvent(syncStartEvent) getTextRenderInfo({ - text: this.text, + text: this.currentText, font: this.font, fontSize: this.fontSize || 0.1, letterSpacing: this.letterSpacing || 0, @@ -432,6 +466,11 @@ const Text = /*#__PURE__*/(() => { }) } + //update dom with latest text + if(this._domElText.textContent !== this.currentText){ + this._domElText.textContent = this.currentText; + } + this.dispatchEvent(syncCompleteEvent) if (callback) { callback() @@ -441,6 +480,15 @@ const Text = /*#__PURE__*/(() => { } } + onAfterRender(){ + if( this.supportScreenReader ){ + this.updateDomPosition() + } + if( this.selectable ){ + this.updateSelectedDomPosition() + } + } + /** * Initiate a sync if needed - note it won't complete until next frame at the * earliest so if possible it's a good idea to call sync() manually as soon as @@ -448,6 +496,8 @@ const Text = /*#__PURE__*/(() => { * @override */ onBeforeRender(renderer, scene, camera, geometry, material, group) { + this.camera = camera + this.renderer = renderer this.sync() // This may not always be a text material, e.g. if there's a scene.overrideMaterial present @@ -533,13 +583,6 @@ const Text = /*#__PURE__*/(() => { this.geometry.detail = detail } - get curveRadius() { - return this.geometry.curveRadius - } - set curveRadius(r) { - this.geometry.curveRadius = r - } - // Create and update material for shadows upon request: get customDepthMaterial() { return first(this.material).getDepthMaterial() @@ -675,31 +718,254 @@ const Text = /*#__PURE__*/(() => { return this.localPositionToTextCoords(this.worldToLocal(position), target) } + /** + * Given a local x/y coordinate in the text block plane, set the start position of the caret + * used in text selection + * @param {number} x + * @param {number} y + * @return {TextCaret | null} + */ + getCaret(x,y){ + let caret = getCaretAtPoint(this.textRenderInfo, x, y) + this.selectionStartIndex = caret.charIndex + this.selectionEndIndex = caret.charIndex + this.updateSelection() + return caret + } + + /** + * Given a local x/y coordinate in the text block plane, set the end position of the caret + * used in text selection + * @param {number} x + * @param {number} y + * @return {TextCaret | null} + */ + testCaret(x,y){ + let caret = getCaretAtPoint(this.textRenderInfo, x, y) + this.selectionEndIndex = caret.charIndex + this.updateSelection() + return caret + } + + /** + * update the selection visually and everything related to copy /paste + */ + updateSelection() { + if(this.selectable){ + this.selectedText = this.text.substring(this.selectionStartIndex,this.selectionEndIndex) + this.selectionRect = getSelectionRects(this._textRenderInfo,this.selectionStartIndex,this.selectionEndIndex) + this._domElSelectedText.textContent = this.selectedText + this.selectDomText() + this.updateSelectedDomPosition() + }else{ + this.selectedText = null + this.selectionRect = [] + } + } + + /** + * Select the text contened in _domElSelectedText in order for it to reflect what's currently selected in the Text + */ + selectDomText(){ + this.highlightText( this.selectionRect) + const sel = document.getSelection() + sel.removeAllRanges() + const range = document.createRange() + range.selectNodeContents(this._domElSelectedText); //sets Range + sel.removeAllRanges(); //remove all ranges from selection + sel.addRange(range); + } + + /** + * update the position of the overlaying HTML that contain all the text that need to be accessible to screen readers + */ + updateDomPosition(){ + let bbox = this.renderer.domElement.getBoundingClientRect() + let width = bbox.width + let height = bbox.height + let left = bbox.left + let top = bbox.top + var widthHalf = width / 2, heightHalf = height / 2; + + var max = new Vector3(0,0,0); + var min = new Vector3(0,0,0); + this.geometry.computeBoundingBox() + max.copy(this.geometry.boundingBox.max).applyMatrix4( this.matrixWorld ); + max = max.project(this.camera); + min.copy(this.geometry.boundingBox.min).applyMatrix4( this.matrixWorld ); + min = min.project(this.camera); + + max.x = ( max.x * widthHalf ) + widthHalf; + max.y = - ( max.y * heightHalf ) + heightHalf; + min.x = ( min.x * widthHalf ) + widthHalf; + min.y = - ( min.y * heightHalf ) + heightHalf; + + this._domElText.style.left = Math.min(min.x,max.x)+left+'px'; + this._domElText.style.top = Math.min(min.y,max.y)+top+'px'; + this._domElText.style.width = Math.abs(max.x-min.x)+'px'; + this._domElText.style.height = Math.abs(max.y-min.y)+'px'; + } + + /** + * update the position of the overlaying HTML that contain + * the selected text in order for it to be acessible through context menu copy + */ + updateSelectedDomPosition(){ + if(this.children.length === 0){ + return + } + + let bbox = this.renderer.domElement.getBoundingClientRect() + let width = bbox.width + let height = bbox.height + let left = bbox.left + let top = bbox.top + var widthHalf = width / 2, heightHalf = height / 2; + + var max = new Vector3(0,0,0); + var min = new Vector3(0,0,0); + + let i=0; + for (let key in this.selectionRect) { + if(i===0){ + max.x = Math.max(this.selectionRect[key].left,this.selectionRect[key].right); + max.y = Math.max(this.selectionRect[key].top,this.selectionRect[key].bottom); + max.z = 0; + min.x = Math.min(this.selectionRect[key].left,this.selectionRect[key].right); + min.y = Math.min(this.selectionRect[key].top,this.selectionRect[key].bottom); + min.z = 0; + }else{ + max.x = Math.max(max.x,this.selectionRect[key].left,this.selectionRect[key].right); + max.y = Math.max(max.y,this.selectionRect[key].top,this.selectionRect[key].bottom); + max.z = Math.max(0,max.z); + min.x = Math.min(min.x,this.selectionRect[key].left,this.selectionRect[key].right); + min.y = Math.min(min.y,this.selectionRect[key].top,this.selectionRect[key].bottom); + min.z = Math.min(0,min.z); + } + i++; + } + + //todo, adjust with text position + // max.x+=1 + // min.x+=1 + // max.y+=-1 + // min.y+=-1 + // max.z+=3 + // min.z+=3 + + max = max.project(this.camera); + min = min.project(this.camera); + + max.x = ( max.x * widthHalf ) + widthHalf; + max.y = - ( max.y * heightHalf ) + heightHalf; + min.x = ( min.x * widthHalf ) + widthHalf; + min.y = - ( min.y * heightHalf ) + heightHalf; + + this._domElSelectedText.style.left = Math.min(min.x,max.x)+left+'px'; + this._domElSelectedText.style.top = Math.min(min.y,max.y)+top+'px'; + this._domElSelectedText.style.width = Math.abs(max.x-min.x)+'px'; + this._domElSelectedText.style.height = Math.abs(max.y-min.y)+'px'; + } + + /** + * visually update the rendering of the text selection in the renderer context + */ + highlightText(selectionRects) { + + let depth = 0.5; + + //todo manage rect update in a cleaner way. Currently we recreate everything everytime + this.children = [] + + for (let key in selectionRects) { + let material = createDerivedMaterial( + new MeshBasicMaterial({ + transparent: true, + opacity: 0.3, + depthWrite: false + }), + { + uniforms: { + rect: {value: new Vector4(selectionRects[key].left,selectionRects[key].top,selectionRects[key].right,selectionRects[key].bottom)}, + depthAndCurveRadius: {value: new Vector2(depth,this.curveRadius)} + }, + vertexDefs: ` + uniform vec4 rect; + uniform vec2 depthAndCurveRadius; + `, + vertexTransform: ` + float depth = depthAndCurveRadius.x; + float rad = depthAndCurveRadius.y; + position.x = mix(rect.x, rect.z, position.x); + position.y = mix(rect.w, rect.y, position.y); + position.z = mix(-depth * 0.5, depth * 0.5, position.z); + if (rad != 0.0) { + float angle = position.x / rad; + position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); + // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); + } + ` + } + ) + material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] + let selectRect = new Mesh( + new BoxBufferGeometry(1, 1, 0.1, 32).translate(0.5, 0.5, 0.5), + material + // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) + ) + this.add(selectRect) + } + + } + + /** + * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context + */ + startObservingMutation(){ + //todo right now each Text class has its own MutationObserver, maybe it cn cause issues if used with multiple Text + this.observer = new MutationObserver(this.mutationCallback.bind(this)); + // Start observing the target node for change ( e.g. page translate ) + this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); + } + + /** + * When a change occurs on the overlaying HTML, it reflect it in the renderer context + */ + mutationCallback(mutationsList, observer) { + if(this._domElText.textContent != this.currentText){ + this.currentText = this._domElText.textContent + console.log(this.currentText) + this._needsSync = true; + this.sync(()=>{ + this.selectedText != '' ? this.updateSelection() : null + }) + } + } + + /** + * stop monitoring dom change + */ + stopObservingMutation(){ + this.observer.disconnect(); + } + /** * @override Custom raycasting to test against the whole text block's max rectangular bounds * TODO is there any reason to make this more granular, like within individual line or glyph rects? */ raycast(raycaster, intersects) { - const {textRenderInfo, curveRadius} = this - if (textRenderInfo) { - const bounds = textRenderInfo.blockBounds - const raycastMesh = curveRadius ? getCurvedRaycastMesh() : getFlatRaycastMesh() - const geom = raycastMesh.geometry - const {position, uv} = geom.attributes - for (let i = 0; i < uv.count; i++) { - let x = bounds[0] + (uv.getX(i) * (bounds[2] - bounds[0])) - const y = bounds[1] + (uv.getY(i) * (bounds[3] - bounds[1])) - let z = 0 - if (curveRadius) { - z = curveRadius - Math.cos(x / curveRadius) * curveRadius - x = Math.sin(x / curveRadius) * curveRadius - } - position.setXYZ(i, x, y, z) - } - geom.boundingSphere = this.geometry.boundingSphere - geom.boundingBox = this.geometry.boundingBox - raycastMesh.matrixWorld = this.matrixWorld - raycastMesh.material.side = this.material.side + const textInfo = this.textRenderInfo + if (textInfo) { + const bounds = textInfo.blockBounds + raycastMesh.matrixWorld.multiplyMatrices( + this.matrixWorld, + tempMat4.set( + bounds[2] - bounds[0], 0, 0, bounds[0], + 0, bounds[3] - bounds[1], 0, bounds[1], + 0, 0, 1, 0, + 0, 0, 0, 1 + ) + ) tempArray.length = 0 raycastMesh.raycast(raycaster, tempArray) for (let i = 0; i < tempArray.length; i++) { @@ -727,6 +993,7 @@ const Text = /*#__PURE__*/(() => { } + // Create setters for properties that affect text layout: SYNCABLE_PROPS.forEach(prop => { const privateKey = '_private_' + prop From b410f0d741a89ed3d997e955479508139704d733 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Mon, 1 Mar 2021 11:55:16 +0100 Subject: [PATCH 02/27] react to radius changes --- packages/troika-three-text/src/Text.js | 73 +++++++++++++++++++------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index a1cbb529..07f8bdff 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -37,10 +37,22 @@ const Text = /*#__PURE__*/(() => { return Array.isArray(o) ? o[0] : o } - const raycastMesh = new Mesh( - new PlaneBufferGeometry(1, 1).translate(0.5, 0.5, 0), - defaultMaterial - ) + let getFlatRaycastMesh = () => { + const mesh = new Mesh( + new PlaneBufferGeometry(1, 1), + defaultMaterial + ) + getFlatRaycastMesh = () => mesh + return mesh + } + let getCurvedRaycastMesh = () => { + const mesh = new Mesh( + new PlaneBufferGeometry(1, 1, 32, 1), + defaultMaterial + ) + getCurvedRaycastMesh = () => mesh + return mesh + } const syncStartEvent = {type: 'syncstart'} const syncCompleteEvent = {type: 'synccomplete'} @@ -500,6 +512,10 @@ const Text = /*#__PURE__*/(() => { this.renderer = renderer this.sync() + if( this.selectable ){ + this.updateHighlightTextUniforms() + } + // This may not always be a text material, e.g. if there's a scene.overrideMaterial present if (material.isTroikaTextMaterial) { this._prepareForRender(material) @@ -583,6 +599,13 @@ const Text = /*#__PURE__*/(() => { this.geometry.detail = detail } + get curveRadius() { + return this.geometry.curveRadius + } + set curveRadius(r) { + this.geometry.curveRadius = r + } + // Create and update material for shadows upon request: get customDepthMaterial() { return first(this.material).getDepthMaterial() @@ -872,7 +895,7 @@ const Text = /*#__PURE__*/(() => { */ highlightText(selectionRects) { - let depth = 0.5; + let depth = 0.4; //todo manage rect update in a cleaner way. Currently we recreate everything everytime this.children = [] @@ -913,11 +936,17 @@ const Text = /*#__PURE__*/(() => { material // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) ) - this.add(selectRect) + this.add(selectRect) } } + updateHighlightTextUniforms(){ + this.children.forEach((selectRect)=>{ + selectRect.material.uniforms.depthAndCurveRadius.value.y = this.curveRadius + }) + } + /** * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context */ @@ -954,18 +983,26 @@ const Text = /*#__PURE__*/(() => { * TODO is there any reason to make this more granular, like within individual line or glyph rects? */ raycast(raycaster, intersects) { - const textInfo = this.textRenderInfo - if (textInfo) { - const bounds = textInfo.blockBounds - raycastMesh.matrixWorld.multiplyMatrices( - this.matrixWorld, - tempMat4.set( - bounds[2] - bounds[0], 0, 0, bounds[0], - 0, bounds[3] - bounds[1], 0, bounds[1], - 0, 0, 1, 0, - 0, 0, 0, 1 - ) - ) + const {textRenderInfo, curveRadius} = this + if (textRenderInfo) { + const bounds = textRenderInfo.blockBounds + const raycastMesh = curveRadius ? getCurvedRaycastMesh() : getFlatRaycastMesh() + const geom = raycastMesh.geometry + const {position, uv} = geom.attributes + for (let i = 0; i < uv.count; i++) { + let x = bounds[0] + (uv.getX(i) * (bounds[2] - bounds[0])) + const y = bounds[1] + (uv.getY(i) * (bounds[3] - bounds[1])) + let z = 0 + if (curveRadius) { + z = curveRadius - Math.cos(x / curveRadius) * curveRadius + x = Math.sin(x / curveRadius) * curveRadius + } + position.setXYZ(i, x, y, z) + } + geom.boundingSphere = this.geometry.boundingSphere + geom.boundingBox = this.geometry.boundingBox + raycastMesh.matrixWorld = this.matrixWorld + raycastMesh.material.side = this.material.side tempArray.length = 0 raycastMesh.raycast(raycaster, tempArray) for (let i = 0; i < tempArray.length; i++) { From 2fabe96e0873e9587b44695725d9d43ec94c2859 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Mon, 1 Mar 2021 15:39:57 +0100 Subject: [PATCH 03/27] text selection color --- .../troika-3d-text/src/facade/Text3DFacade.js | 1 + packages/troika-examples/text/TextExample.jsx | 3 + packages/troika-three-text/src/Text.js | 61 +++++++++++++------ 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index d977acb9..72ecf69c 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -18,6 +18,7 @@ const TEXT_MESH_PROPS = [ 'whiteSpace', 'material', 'color', + 'selectionColor', 'colorRanges', 'fillOpacity', 'outlineOpacity', diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index d46ec05a..0c326de9 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -104,6 +104,7 @@ class TextExample extends React.Component { anchorX: 'center', anchorY: 'middle', color: 0xffffff, + selectionColor: 0xff0000, fillOpacity: 1, strokeOpacity: 1, strokeColor: 0x808080, @@ -216,6 +217,7 @@ class TextExample extends React.Component { supportScreenReader: state.supportScreenReader, material: material, color: 0xffffff, + selectionColor: state.selectionColor, scaleX: state.textScale || 1, scaleY: state.textScale || 1, scaleZ: state.textScale || 1, @@ -316,6 +318,7 @@ class TextExample extends React.Component { {type: 'boolean', path: "shadows", label: "Shadows"}, {type: 'boolean', path: "colorRanges", label: "colorRanges (WIP)"}, {type: 'boolean', path: "selectable", label: "Selectable (WIP)"}, + {type: 'select', path: 'selectionColor', label: "Selection Color (WIP)", options: ['white','red','blue']}, {type: 'boolean', path: "supportScreenReader", label: "Support Screen readers (WIP)"}, {type: 'number', path: "fontSize", label: "fontSize", min: 0.01, max: 0.2, step: 0.01}, {type: 'number', path: "textScale", label: "scale", min: 0.1, max: 10, step: 0.1}, diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 07f8bdff..ca8ff04a 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -25,6 +25,7 @@ const Text = /*#__PURE__*/(() => { transparent: true }) const defaultStrokeColor = 0x808080 + const defaultSelectionColor = 0xffffff const tempMat4 = new Matrix4() const tempVec3a = new Vector3() @@ -114,7 +115,7 @@ const Text = /*#__PURE__*/(() => { this.startObservingMutation() - this.selectionRect = [] + this.selectionRects = [] //TODO test html support @@ -266,6 +267,15 @@ const Text = /*#__PURE__*/(() => { */ this.color = null + /** + * @member {string|number|THREE.Color} selectionColor + * This is a shortcut for setting the `color` of the text's material. You can use this + * if you don't want to specify a whole custom `material`. Also, if you do use a custom + * `material`, this color will only be used for this particuar Text instance, even if + * that same material instance is shared across multiple Text objects. + */ + this.selectionColor = defaultSelectionColor + /** * @member {object|null} colorRanges * WARNING: This API is experimental and may change. @@ -776,13 +786,13 @@ const Text = /*#__PURE__*/(() => { updateSelection() { if(this.selectable){ this.selectedText = this.text.substring(this.selectionStartIndex,this.selectionEndIndex) - this.selectionRect = getSelectionRects(this._textRenderInfo,this.selectionStartIndex,this.selectionEndIndex) + this.selectionRects = getSelectionRects(this._textRenderInfo,this.selectionStartIndex,this.selectionEndIndex) this._domElSelectedText.textContent = this.selectedText this.selectDomText() this.updateSelectedDomPosition() }else{ this.selectedText = null - this.selectionRect = [] + this.selectionRects = [] } } @@ -790,7 +800,7 @@ const Text = /*#__PURE__*/(() => { * Select the text contened in _domElSelectedText in order for it to reflect what's currently selected in the Text */ selectDomText(){ - this.highlightText( this.selectionRect) + this.highlightText() const sel = document.getSelection() sel.removeAllRanges() const range = document.createRange() @@ -849,20 +859,20 @@ const Text = /*#__PURE__*/(() => { var min = new Vector3(0,0,0); let i=0; - for (let key in this.selectionRect) { + for (let key in this.selectionRects) { if(i===0){ - max.x = Math.max(this.selectionRect[key].left,this.selectionRect[key].right); - max.y = Math.max(this.selectionRect[key].top,this.selectionRect[key].bottom); + max.x = Math.max(this.selectionRects[key].left,this.selectionRects[key].right); + max.y = Math.max(this.selectionRects[key].top,this.selectionRects[key].bottom); max.z = 0; - min.x = Math.min(this.selectionRect[key].left,this.selectionRect[key].right); - min.y = Math.min(this.selectionRect[key].top,this.selectionRect[key].bottom); + min.x = Math.min(this.selectionRects[key].left,this.selectionRects[key].right); + min.y = Math.min(this.selectionRects[key].top,this.selectionRects[key].bottom); min.z = 0; }else{ - max.x = Math.max(max.x,this.selectionRect[key].left,this.selectionRect[key].right); - max.y = Math.max(max.y,this.selectionRect[key].top,this.selectionRect[key].bottom); + max.x = Math.max(max.x,this.selectionRects[key].left,this.selectionRects[key].right); + max.y = Math.max(max.y,this.selectionRects[key].top,this.selectionRects[key].bottom); max.z = Math.max(0,max.z); - min.x = Math.min(min.x,this.selectionRect[key].left,this.selectionRect[key].right); - min.y = Math.min(min.y,this.selectionRect[key].top,this.selectionRect[key].bottom); + min.x = Math.min(min.x,this.selectionRects[key].left,this.selectionRects[key].right); + min.y = Math.min(min.y,this.selectionRects[key].top,this.selectionRects[key].bottom); min.z = Math.min(0,min.z); } i++; @@ -893,23 +903,24 @@ const Text = /*#__PURE__*/(() => { /** * visually update the rendering of the text selection in the renderer context */ - highlightText(selectionRects) { + highlightText() { let depth = 0.4; //todo manage rect update in a cleaner way. Currently we recreate everything everytime this.children = [] - for (let key in selectionRects) { + for (let key in this.selectionRects) { let material = createDerivedMaterial( new MeshBasicMaterial({ + color:this.selectionColor, transparent: true, opacity: 0.3, depthWrite: false }), { uniforms: { - rect: {value: new Vector4(selectionRects[key].left,selectionRects[key].top,selectionRects[key].right,selectionRects[key].bottom)}, + rect: {value: new Vector4(this.selectionRects[key].left,this.selectionRects[key].top,this.selectionRects[key].right,this.selectionRects[key].bottom)}, depthAndCurveRadius: {value: new Vector2(depth,this.curveRadius)} }, vertexDefs: ` @@ -938,13 +949,23 @@ const Text = /*#__PURE__*/(() => { ) this.add(selectRect) } - } updateHighlightTextUniforms(){ - this.children.forEach((selectRect)=>{ - selectRect.material.uniforms.depthAndCurveRadius.value.y = this.curveRadius - }) + for (let key in this.selectionRects) { + console.log(this.children[key].material) + this.children[key].material.uniforms.depthAndCurveRadius.value.y = this.curveRadius + this.children[key].material.uniforms.rect.value.x = this.selectionRects[key].left + this.children[key].material.uniforms.rect.value.y = this.selectionRects[key].top + this.children[key].material.uniforms.rect.value.z = this.selectionRects[key].right + this.children[key].material.uniforms.rect.value.w = this.selectionRects[key].bottom + if(this.selectionColor != this.children[key].material.color){ + //faster to check fo color change or to set needsUpdate true each time ? + //to discuss + this.children[key].material.color.set(this.selectionColor) + this.children[key].material.needsUpdate = true + } + } } /** From aa2edfa3bcad87e749ee894998e676ec61f4631f Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Mon, 1 Mar 2021 15:51:26 +0100 Subject: [PATCH 04/27] demo fix and selection thikness --- packages/troika-examples/text/TextExample.jsx | 2 +- packages/troika-three-text/src/Text.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index 0c326de9..c3de898e 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -104,7 +104,7 @@ class TextExample extends React.Component { anchorX: 'center', anchorY: 'middle', color: 0xffffff, - selectionColor: 0xff0000, + selectionColor: 'white', fillOpacity: 1, strokeOpacity: 1, strokeColor: 0x808080, diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index ca8ff04a..8fb8ee12 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -905,7 +905,7 @@ const Text = /*#__PURE__*/(() => { */ highlightText() { - let depth = 0.4; + let THICKNESS = 0.25; //todo manage rect update in a cleaner way. Currently we recreate everything everytime this.children = [] @@ -921,7 +921,10 @@ const Text = /*#__PURE__*/(() => { { uniforms: { rect: {value: new Vector4(this.selectionRects[key].left,this.selectionRects[key].top,this.selectionRects[key].right,this.selectionRects[key].bottom)}, - depthAndCurveRadius: {value: new Vector2(depth,this.curveRadius)} + depthAndCurveRadius: {value: new Vector2( + (this.selectionRects[key].top - this.selectionRects[key].bottom)*THICKNESS, + this.curveRadius + )} }, vertexDefs: ` uniform vec4 rect; @@ -953,7 +956,6 @@ const Text = /*#__PURE__*/(() => { updateHighlightTextUniforms(){ for (let key in this.selectionRects) { - console.log(this.children[key].material) this.children[key].material.uniforms.depthAndCurveRadius.value.y = this.curveRadius this.children[key].material.uniforms.rect.value.x = this.selectionRects[key].left this.children[key].material.uniforms.rect.value.y = this.selectionRects[key].top From 128d4c3bbdc91d6801c295357067e3ecef153d63 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Mon, 1 Mar 2021 22:32:40 +0100 Subject: [PATCH 05/27] fix selection child transform --- .../src/facade/SelectionManagerFacade.js | 5 +++-- packages/troika-three-text/src/Text.js | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index 31e0c969..7a288a01 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -18,6 +18,7 @@ class SelectionManagerFacade extends ListFacade { constructor (parent, onSelectionChange) { super(parent) const textMesh = parent.threeObject + console.log(textMesh) this.rangeColor = 0x00ccff this.clipRect = noClip @@ -47,7 +48,7 @@ class SelectionManagerFacade extends ListFacade { const textRenderInfo = textMesh.textRenderInfo if (textRenderInfo) { const textPos = textMesh.worldPositionToTextCoords(e.intersection.point, tempVec2) - const caret = textMesh.getCaret(textPos.x, textPos.y) + const caret = textMesh.startCaret(textPos.x, textPos.y) if (caret) { onSelectionChange(caret.charIndex, caret.charIndex) parent.addEventListener('drag', onDrag) @@ -74,7 +75,7 @@ class SelectionManagerFacade extends ListFacade { textPos = ray.intersectPlane(tempPlane.setComponents(0, 0, 1, 0), tempVec3) } if (textPos) { - const caret = textMesh.testCaret(textPos.x, textPos.y) + const caret = textMesh.moveCaret(textPos.x, textPos.y) if (caret) { onSelectionChange(this.selectionStart, caret.charIndex) } diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 8fb8ee12..48ce2d63 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -758,7 +758,7 @@ const Text = /*#__PURE__*/(() => { * @param {number} y * @return {TextCaret | null} */ - getCaret(x,y){ + startCaret(x,y){ let caret = getCaretAtPoint(this.textRenderInfo, x, y) this.selectionStartIndex = caret.charIndex this.selectionEndIndex = caret.charIndex @@ -773,7 +773,7 @@ const Text = /*#__PURE__*/(() => { * @param {number} y * @return {TextCaret | null} */ - testCaret(x,y){ + moveCaret(x,y){ let caret = getCaretAtPoint(this.textRenderInfo, x, y) this.selectionEndIndex = caret.charIndex this.updateSelection() @@ -920,7 +920,12 @@ const Text = /*#__PURE__*/(() => { }), { uniforms: { - rect: {value: new Vector4(this.selectionRects[key].left,this.selectionRects[key].top,this.selectionRects[key].right,this.selectionRects[key].bottom)}, + rect: {value: new Vector4( + this.selectionRects[key].left , + this.selectionRects[key].top , + this.selectionRects[key].right , + this.selectionRects[key].bottom + )}, depthAndCurveRadius: {value: new Vector2( (this.selectionRects[key].top - this.selectionRects[key].bottom)*THICKNESS, this.curveRadius @@ -950,8 +955,11 @@ const Text = /*#__PURE__*/(() => { material // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) ) + // selectRect.position.x = -1 + // selectRect.position.y = -1 this.add(selectRect) } + this.updateWorldMatrix(false,true) } updateHighlightTextUniforms(){ From 09499cef91510602a9ca69c0ccc2cd0a79cd53c2 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Tue, 2 Mar 2021 18:30:14 +0100 Subject: [PATCH 06/27] clear selection + selection material --- .../src/facade/SelectionManagerFacade.js | 18 +++- .../troika-3d-text/src/facade/Text3DFacade.js | 3 +- packages/troika-examples/text/TextExample.jsx | 5 +- packages/troika-three-text/src/Text.js | 102 ++++++++++++++++-- 4 files changed, 114 insertions(+), 14 deletions(-) diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index 7a288a01..1ebe6f98 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -2,6 +2,7 @@ import { ListFacade } from 'troika-3d' import { Matrix4, Plane, Vector2, Vector3 } from 'three' import { invertMatrix4 } from 'troika-three-utils' import SelectionRangeRect from './SelectionRangeRect.js' +import { Mesh } from '../../../../node_modules/three/src/Three.js' const THICKNESS = 0.25 //rect depth as percentage of height @@ -71,8 +72,8 @@ class SelectionManagerFacade extends ListFacade { if (ix && ix.object === textMesh && ix.point) { textPos = textMesh.worldPositionToTextCoords(ix.point, tempVec2) } else { - const ray = e.ray.clone().applyMatrix4(invertMatrix4(textMesh.matrixWorld, tempMat4)) - textPos = ray.intersectPlane(tempPlane.setComponents(0, 0, 1, 0), tempVec3) + // const ray = e.ray.clone().applyMatrix4(invertMatrix4(textMesh.matrixWorld, tempMat4)) + // textPos = ray.intersectPlane(tempPlane.setComponents(0, 0, 1, 0), tempVec3) } if (textPos) { const caret = textMesh.moveCaret(textPos.x, textPos.y) @@ -89,6 +90,19 @@ class SelectionManagerFacade extends ListFacade { parent.removeEventListener('dragend', onDragEnd) } + //clear selection if missed click + parent.getSceneFacade().addEventListener('click',(e)=>{ + let target = e.target + do { + if(target.$facadeId === textMesh.parent.$facade.$facadeId){ + return + } + target = target.parent + } while (target !== null) + //clear selection + textMesh.startCaret(0,0) + }) + parent.addEventListener('dragstart', onDragStart) parent.addEventListener('mousedown', onDragStart) var canvas = parent.getSceneFacade().parent._threeRenderer.domElement; diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index 72ecf69c..789de1f8 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -38,7 +38,8 @@ const TEXT_MESH_PROPS = [ 'sdfGlyphSize', 'debugSDF', 'supportScreenReader', - 'selectable' + 'selectable', + 'selectionMaterial' ] diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index c3de898e..be62669f 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -193,6 +193,8 @@ class TextExample extends React.Component { facade: Text3DFacade, castShadow: state.shadows, text: TEXTS[state.text], + x:0.5, + y:0.5, font: FONTS[state.font], fontSize: state.fontSize, maxWidth: state.maxWidth, @@ -217,7 +219,8 @@ class TextExample extends React.Component { supportScreenReader: state.supportScreenReader, material: material, color: 0xffffff, - selectionColor: state.selectionColor, + selectionMaterial: null, + selectionColor: 0xfffff, scaleX: state.textScale || 1, scaleY: state.textScale || 1, scaleZ: state.textScale || 1, diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 48ce2d63..743b0ce3 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -8,7 +8,8 @@ import { Vector4, Vector3, Vector2, - BoxBufferGeometry + BoxBufferGeometry, + BoxHelper } from 'three' import { GlyphsGeometry } from './GlyphsGeometry.js' import { createTextDerivedMaterial } from './TextDerivedMaterial.js' @@ -97,6 +98,9 @@ const Text = /*#__PURE__*/(() => { this._domElSelectedText = document.createElement('p') this._domElText = document.createElement(this.tagName ? this.tagName : 'p') + this._domElText2 = document.createElement(this.tagName ? this.tagName : 'p') + this._domElText3 = document.createElement(this.tagName ? this.tagName : 'p') + this._domElText4 = document.createElement(this.tagName ? this.tagName : 'p') this.selectionStartIndex = 0; this.selectionEndIndex = 0; this.selectedText = null; @@ -107,10 +111,16 @@ const Text = /*#__PURE__*/(() => { }else{ document.body.appendChild(this._domElSelectedText) document.body.appendChild(this._domElText) + document.body.appendChild(this._domElText2) + document.body.appendChild(this._domElText3) + document.body.appendChild(this._domElText4) } this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' + this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' + this._domElText2.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' + this._domElText3.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' + this._domElText4.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' this.startObservingMutation() @@ -258,6 +268,18 @@ const Text = /*#__PURE__*/(() => { */ this.material = null + /** + * @member {THREE.Material} selectionMaterial + * Defines a _base_ material to be used when rendering the text. This material will be + * automatically replaced with a material derived from it, that adds shader code to + * decrease the alpha for each fragment (pixel) outside the text glyphs, with antialiasing. + * By default it will derive from a simple white MeshBasicMaterial, but you can use any + * of the other mesh materials to gain other features like lighting, texture maps, etc. + * + * Also see the `selectionColor` shortcut property. + */ + this.selectionMaterial = null + /** * @member {string|number|THREE.Color} color * This is a shortcut for setting the `color` of the text's material. You can use this @@ -509,6 +531,21 @@ const Text = /*#__PURE__*/(() => { if( this.selectable ){ this.updateSelectedDomPosition() } + if(!this.pasencore){ + this.box = new BoxHelper( this, 0xffff00 ); + this.parent.add( this.box ); + this.pasencore=true + + }else{ + this.box.update() + // this.geometry.applyMatrix4(this.matrixWorld) + // const position = this.geometry.getAttribute('position').array + // var test = new Vector3(position[0],position[1],position[2]); + // test.applyMatrix4(this.matrixWorld) + // console.log(test) + // console.log(this.geometry.getAttribute('position')) + // console.log(this.geometry.attributes.position) + } } /** @@ -824,19 +861,64 @@ const Text = /*#__PURE__*/(() => { var min = new Vector3(0,0,0); this.geometry.computeBoundingBox() max.copy(this.geometry.boundingBox.max).applyMatrix4( this.matrixWorld ); - max = max.project(this.camera); + max.project(this.camera); min.copy(this.geometry.boundingBox.min).applyMatrix4( this.matrixWorld ); - min = min.project(this.camera); + min.project(this.camera); max.x = ( max.x * widthHalf ) + widthHalf; max.y = - ( max.y * heightHalf ) + heightHalf; min.x = ( min.x * widthHalf ) + widthHalf; min.y = - ( min.y * heightHalf ) + heightHalf; - this._domElText.style.left = Math.min(min.x,max.x)+left+'px'; - this._domElText.style.top = Math.min(min.y,max.y)+top+'px'; - this._domElText.style.width = Math.abs(max.x-min.x)+'px'; - this._domElText.style.height = Math.abs(max.y-min.y)+'px'; + const position = this.geometry.getAttribute('position').array + var bottomLeftVert = new Vector3(position[0],position[1],position[2]); + var bottomRightVert = new Vector3(position[3],position[4],position[5]); + var topLeftVert = new Vector3(position[6],position[7],position[8]); + var topRightVert = new Vector3(position[9],position[10],position[11]); + bottomLeftVert.applyMatrix4(this.matrixWorld) + bottomRightVert.applyMatrix4(this.matrixWorld) + topLeftVert.applyMatrix4(this.matrixWorld) + topRightVert.applyMatrix4(this.matrixWorld) + bottomLeftVert.project(this.camera); + bottomRightVert.project(this.camera); + topLeftVert.project(this.camera); + topRightVert.project(this.camera); + // console.log(this.geometry,position,bottomLeftVert,bottomRightVert,topLeftVert,topRightVert) + // console.log( + // Math.min(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x), + // widthHalf, + // left, + // Math.min(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x)* widthHalf+widthHalf+left+'px' + // ) + // console.log(min,max) + // debugger; + // let minX = Math.min(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x)* -widthHalf+widthHalf + // let minY = Math.min(bottomLeftVert.y,bottomRightVert.y,topRightVert.y,topLeftVert.y)*-heightHalf-heightHalf + // let maxX = Math.max(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x)* widthHalf+widthHalf + // let maxY = -Math.max(bottomLeftVert.y,bottomRightVert.y,topRightVert.y,topLeftVert.y)*heightHalf+heightHalf + // let halfPlaneX = Math.abs(maxX-minX)/2; + // let halfPlaneY = Math.abs(maxY-minY)/2; + + let minX = bottomLeftVert.x* widthHalf+widthHalf + let minY = bottomLeftVert.y*-heightHalf+heightHalf + this._domElText.style.left = (bottomLeftVert.x* widthHalf+widthHalf)+left+'px'; + this._domElText.style.top = (bottomLeftVert.y*-heightHalf+heightHalf)+top+'px'; + this._domElText2.style.left = (bottomRightVert.x* widthHalf+widthHalf)+left+'px'; + this._domElText2.style.top = (bottomRightVert.y*-heightHalf+heightHalf)+top+'px'; + this._domElText3.style.left = (topLeftVert.x* widthHalf+widthHalf)+left+'px'; + this._domElText3.style.top = (topLeftVert.y*-heightHalf+heightHalf)+top+'px'; + this._domElText4.style.left = (topRightVert.x* widthHalf+widthHalf)+left+'px'; + this._domElText4.style.top = (topRightVert.y*-heightHalf+heightHalf)+top+'px'; + // this._domElText.style.left = Math.min(min.x,max.x)+left+'px'; + // this._domElText.style.top = Math.min(min.y,max.y)+top+'px'; + this._domElText.style.width = 10+'px'; + this._domElText.style.height = 10+'px'; + this._domElText2.style.width = 10+'px'; + this._domElText2.style.height = 10+'px'; + this._domElText3.style.width = 10+'px'; + this._domElText3.style.height = 10+'px'; + this._domElText4.style.width = 10+'px'; + this._domElText4.style.height = 10+'px'; } /** @@ -912,8 +994,8 @@ const Text = /*#__PURE__*/(() => { for (let key in this.selectionRects) { let material = createDerivedMaterial( - new MeshBasicMaterial({ - color:this.selectionColor, + this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ + color:this.selectionColor ? this.selectionColor : defaultSelectionColor, transparent: true, opacity: 0.3, depthWrite: false From cb24d4c475753856f1ab391e28d1a984ff8aabbf Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Thu, 4 Mar 2021 14:56:39 +0100 Subject: [PATCH 07/27] html correct position --- .../src/facade/SelectionManagerFacade.js | 1 - packages/troika-examples/text/TextExample.jsx | 2 +- packages/troika-three-text/src/Text.js | 107 +++++++----------- 3 files changed, 43 insertions(+), 67 deletions(-) diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index 1ebe6f98..6aab803a 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -2,7 +2,6 @@ import { ListFacade } from 'troika-3d' import { Matrix4, Plane, Vector2, Vector3 } from 'three' import { invertMatrix4 } from 'troika-three-utils' import SelectionRangeRect from './SelectionRangeRect.js' -import { Mesh } from '../../../../node_modules/three/src/Three.js' const THICKNESS = 0.25 //rect depth as percentage of height diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index be62669f..0662b1ca 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -117,7 +117,7 @@ class TextExample extends React.Component { curveRadius: 0, fog: false, animTextColor: true, - animTilt: true, + animTilt: false, animRotate: false, material: 'MeshStandardMaterial', useTexture: false, diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 9b5a4ead..a31d2336 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -98,9 +98,6 @@ const Text = /*#__PURE__*/(() => { this._domElSelectedText = document.createElement('p') this._domElText = document.createElement(this.tagName ? this.tagName : 'p') - this._domElText2 = document.createElement(this.tagName ? this.tagName : 'p') - this._domElText3 = document.createElement(this.tagName ? this.tagName : 'p') - this._domElText4 = document.createElement(this.tagName ? this.tagName : 'p') this.selectionStartIndex = 0; this.selectionEndIndex = 0; this.selectedText = null; @@ -111,16 +108,10 @@ const Text = /*#__PURE__*/(() => { }else{ document.body.appendChild(this._domElSelectedText) document.body.appendChild(this._domElText) - document.body.appendChild(this._domElText2) - document.body.appendChild(this._domElText3) - document.body.appendChild(this._domElText4) } this._domElSelectedText.setAttribute('aria-hidden','true') this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' - this._domElText2.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' - this._domElText3.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' - this._domElText4.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' this.startObservingMutation() @@ -860,66 +851,52 @@ const Text = /*#__PURE__*/(() => { var max = new Vector3(0,0,0); var min = new Vector3(0,0,0); + this.geometry.computeBoundingBox() max.copy(this.geometry.boundingBox.max).applyMatrix4( this.matrixWorld ); - max.project(this.camera); min.copy(this.geometry.boundingBox.min).applyMatrix4( this.matrixWorld ); - min.project(this.camera); - - max.x = ( max.x * widthHalf ) + widthHalf; - max.y = - ( max.y * heightHalf ) + heightHalf; - min.x = ( min.x * widthHalf ) + widthHalf; - min.y = - ( min.y * heightHalf ) + heightHalf; - const position = this.geometry.getAttribute('position').array - var bottomLeftVert = new Vector3(position[0],position[1],position[2]); - var bottomRightVert = new Vector3(position[3],position[4],position[5]); - var topLeftVert = new Vector3(position[6],position[7],position[8]); - var topRightVert = new Vector3(position[9],position[10],position[11]); - bottomLeftVert.applyMatrix4(this.matrixWorld) - bottomRightVert.applyMatrix4(this.matrixWorld) - topLeftVert.applyMatrix4(this.matrixWorld) - topRightVert.applyMatrix4(this.matrixWorld) - bottomLeftVert.project(this.camera); - bottomRightVert.project(this.camera); - topLeftVert.project(this.camera); - topRightVert.project(this.camera); - // console.log(this.geometry,position,bottomLeftVert,bottomRightVert,topLeftVert,topRightVert) - // console.log( - // Math.min(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x), - // widthHalf, - // left, - // Math.min(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x)* widthHalf+widthHalf+left+'px' - // ) - // console.log(min,max) - // debugger; - // let minX = Math.min(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x)* -widthHalf+widthHalf - // let minY = Math.min(bottomLeftVert.y,bottomRightVert.y,topRightVert.y,topLeftVert.y)*-heightHalf-heightHalf - // let maxX = Math.max(bottomLeftVert.x,bottomRightVert.x,topRightVert.x,topLeftVert.x)* widthHalf+widthHalf - // let maxY = -Math.max(bottomLeftVert.y,bottomRightVert.y,topRightVert.y,topLeftVert.y)*heightHalf+heightHalf - // let halfPlaneX = Math.abs(maxX-minX)/2; - // let halfPlaneY = Math.abs(maxY-minY)/2; - - let minX = bottomLeftVert.x* widthHalf+widthHalf - let minY = bottomLeftVert.y*-heightHalf+heightHalf - this._domElText.style.left = (bottomLeftVert.x* widthHalf+widthHalf)+left+'px'; - this._domElText.style.top = (bottomLeftVert.y*-heightHalf+heightHalf)+top+'px'; - this._domElText2.style.left = (bottomRightVert.x* widthHalf+widthHalf)+left+'px'; - this._domElText2.style.top = (bottomRightVert.y*-heightHalf+heightHalf)+top+'px'; - this._domElText3.style.left = (topLeftVert.x* widthHalf+widthHalf)+left+'px'; - this._domElText3.style.top = (topLeftVert.y*-heightHalf+heightHalf)+top+'px'; - this._domElText4.style.left = (topRightVert.x* widthHalf+widthHalf)+left+'px'; - this._domElText4.style.top = (topRightVert.y*-heightHalf+heightHalf)+top+'px'; - // this._domElText.style.left = Math.min(min.x,max.x)+left+'px'; - // this._domElText.style.top = Math.min(min.y,max.y)+top+'px'; - this._domElText.style.width = 10+'px'; - this._domElText.style.height = 10+'px'; - this._domElText2.style.width = 10+'px'; - this._domElText2.style.height = 10+'px'; - this._domElText3.style.width = 10+'px'; - this._domElText3.style.height = 10+'px'; - this._domElText4.style.width = 10+'px'; - this._domElText4.style.height = 10+'px'; + var bboxVectors = + [ + new Vector3(max.x,max.y,max.z), + new Vector3(min.x,max.y,max.z), + new Vector3(min.x,min.y,max.z), + new Vector3(max.x,min.y,max.z), + new Vector3(max.x,max.y,min.z), + new Vector3(min.x,max.y,min.z), + new Vector3(min.x,min.y,min.z), + new Vector3(max.x,min.y,min.z) + ] + + let xmin = null + let xmax = null + let ymin = null + let ymax = null + + + bboxVectors.forEach(vec => { + vec.project(this.camera); + }); + xmin = bboxVectors[0].x + xmax = bboxVectors[0].x + ymin = bboxVectors[0].y + ymax = bboxVectors[0].y + bboxVectors.forEach(vec => { + xmin = xmin > vec.x ? vec.x : xmin + xmax = xmax < vec.x ? vec.x : xmax + ymin = ymin > vec.y ? vec.y : ymin + ymax = ymax < vec.y ? vec.y : ymax + }); + + xmax = ( xmax * widthHalf ) + widthHalf; + ymax = - ( ymax * heightHalf ) + heightHalf; + xmin = ( xmin * widthHalf ) + widthHalf; + ymin = - ( ymin * heightHalf ) + heightHalf; + + this._domElText.style.left = xmin+left+'px'; + this._domElText.style.top = ymax+top+'px'; + this._domElText.style.width = Math.abs(xmax-xmin)+'px'; + this._domElText.style.height = Math.abs(ymax-ymin)+'px'; } /** From b8f44d35c63d662b448a0f1b37a00f35ece3e2c6 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Fri, 5 Mar 2021 11:16:50 +0100 Subject: [PATCH 08/27] dom selection position --- packages/troika-examples/text/TextExample.jsx | 4 +- packages/troika-three-text/src/Text.js | 96 +++++++++++-------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index 0662b1ca..f352279c 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -193,8 +193,8 @@ class TextExample extends React.Component { facade: Text3DFacade, castShadow: state.shadows, text: TEXTS[state.text], - x:0.5, - y:0.5, + x:0, + y:0, font: FONTS[state.font], fontSize: state.fontSize, maxWidth: state.maxWidth, diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index a31d2336..c80e5fee 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -8,8 +8,7 @@ import { Vector4, Vector3, Vector2, - BoxBufferGeometry, - BoxHelper + BoxBufferGeometry } from 'three' import { GlyphsGeometry } from './GlyphsGeometry.js' import { createTextDerivedMaterial } from './TextDerivedMaterial.js' @@ -111,7 +110,7 @@ const Text = /*#__PURE__*/(() => { } this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;opacity:0.5;background:#ff0000;' + this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;' this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' this.startObservingMutation() @@ -522,21 +521,6 @@ const Text = /*#__PURE__*/(() => { if( this.selectable ){ this.updateSelectedDomPosition() } - if(!this.pasencore){ - this.box = new BoxHelper( this, 0xffff00 ); - this.parent.add( this.box ); - this.pasencore=true - - }else{ - this.box.update() - // this.geometry.applyMatrix4(this.matrixWorld) - // const position = this.geometry.getAttribute('position').array - // var test = new Vector3(position[0],position[1],position[2]); - // test.applyMatrix4(this.matrixWorld) - // console.log(test) - // console.log(this.geometry.getAttribute('position')) - // console.log(this.geometry.attributes.position) - } } /** @@ -897,6 +881,7 @@ const Text = /*#__PURE__*/(() => { this._domElText.style.top = ymax+top+'px'; this._domElText.style.width = Math.abs(xmax-xmin)+'px'; this._domElText.style.height = Math.abs(ymax-ymin)+'px'; + this._domElText.style.fontSize = Math.abs(ymax-ymin)+'px'; } /** @@ -905,6 +890,8 @@ const Text = /*#__PURE__*/(() => { */ updateSelectedDomPosition(){ if(this.children.length === 0){ + this._domElSelectedText.style.width = '0px'; + this._domElSelectedText.style.height = '0px'; return } @@ -918,46 +905,73 @@ const Text = /*#__PURE__*/(() => { var max = new Vector3(0,0,0); var min = new Vector3(0,0,0); + + this.children[0].geometry.computeBoundingBox() + this.children[this.children.length-1].geometry.computeBoundingBox() + + // max.copy(this.children[0].geometry.boundingBox.max) + // min.copy(this.children[0].geometry.boundingBox.min) + // max.max(this.children[this.children.length-1].geometry.boundingBox.max).applyMatrix4( this.children[this.children.length-1].matrixWorld ); + // min.min(this.children[this.children.length-1].geometry.boundingBox.min).applyMatrix4( this.children[this.children.length-1].matrixWorld ); + let i=0; for (let key in this.selectionRects) { if(i===0){ max.x = Math.max(this.selectionRects[key].left,this.selectionRects[key].right); max.y = Math.max(this.selectionRects[key].top,this.selectionRects[key].bottom); - max.z = 0; + max.z = this.geometry.boundingBox.max.z; min.x = Math.min(this.selectionRects[key].left,this.selectionRects[key].right); min.y = Math.min(this.selectionRects[key].top,this.selectionRects[key].bottom); - min.z = 0; + min.z = this.geometry.boundingBox.min.z; }else{ max.x = Math.max(max.x,this.selectionRects[key].left,this.selectionRects[key].right); max.y = Math.max(max.y,this.selectionRects[key].top,this.selectionRects[key].bottom); - max.z = Math.max(0,max.z); min.x = Math.min(min.x,this.selectionRects[key].left,this.selectionRects[key].right); min.y = Math.min(min.y,this.selectionRects[key].top,this.selectionRects[key].bottom); - min.z = Math.min(0,min.z); } i++; } - //todo, adjust with text position - // max.x+=1 - // min.x+=1 - // max.y+=-1 - // min.y+=-1 - // max.z+=3 - // min.z+=3 + var bboxVectors = + [ + new Vector3(max.x,max.y,max.z).applyMatrix4( this.matrixWorld ), + new Vector3(min.x,max.y,max.z).applyMatrix4( this.matrixWorld ), + new Vector3(min.x,min.y,max.z).applyMatrix4( this.matrixWorld ), + new Vector3(max.x,min.y,max.z).applyMatrix4( this.matrixWorld ), + new Vector3(max.x,max.y,min.z).applyMatrix4( this.matrixWorld ), + new Vector3(min.x,max.y,min.z).applyMatrix4( this.matrixWorld ), + new Vector3(min.x,min.y,min.z).applyMatrix4( this.matrixWorld ), + new Vector3(max.x,min.y,min.z).applyMatrix4( this.matrixWorld ) + ] - max = max.project(this.camera); - min = min.project(this.camera); - - max.x = ( max.x * widthHalf ) + widthHalf; - max.y = - ( max.y * heightHalf ) + heightHalf; - min.x = ( min.x * widthHalf ) + widthHalf; - min.y = - ( min.y * heightHalf ) + heightHalf; - - this._domElSelectedText.style.left = Math.min(min.x,max.x)+left+'px'; - this._domElSelectedText.style.top = Math.min(min.y,max.y)+top+'px'; - this._domElSelectedText.style.width = Math.abs(max.x-min.x)+'px'; - this._domElSelectedText.style.height = Math.abs(max.y-min.y)+'px'; + let xmin = null + let xmax = null + let ymin = null + let ymax = null + + bboxVectors.forEach(vec => { + vec.project(this.camera); + }); + xmin = bboxVectors[0].x + xmax = bboxVectors[0].x + ymin = bboxVectors[0].y + ymax = bboxVectors[0].y + bboxVectors.forEach(vec => { + xmin = xmin > vec.x ? vec.x : xmin + xmax = xmax < vec.x ? vec.x : xmax + ymin = ymin > vec.y ? vec.y : ymin + ymax = ymax < vec.y ? vec.y : ymax + }); + + xmax = ( xmax * widthHalf ) + widthHalf; + ymax = - ( ymax * heightHalf ) + heightHalf; + xmin = ( xmin * widthHalf ) + widthHalf; + ymin = - ( ymin * heightHalf ) + heightHalf; + + this._domElSelectedText.style.left = xmin+left+'px'; + this._domElSelectedText.style.top = ymax+top+'px'; + this._domElSelectedText.style.width = Math.abs(xmax-xmin)+'px'; + this._domElSelectedText.style.height = Math.abs(ymax-ymin)+'px'; } /** From 413e158baf76b55d0976acb2a682c792b3387b15 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Fri, 5 Mar 2021 12:34:31 +0100 Subject: [PATCH 09/27] overlaying html edge cases --- packages/troika-examples/index.css | 3 +++ packages/troika-three-text/src/Text.js | 25 ++++++++++++------------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/troika-examples/index.css b/packages/troika-examples/index.css index 9bf44170..92913303 100644 --- a/packages/troika-examples/index.css +++ b/packages/troika-examples/index.css @@ -4,6 +4,9 @@ html, body { color: #FFF; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; + overflow: hidden; + width: 100vw; + height: 100vh; } .examples { diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index c80e5fee..3d814539 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -101,17 +101,14 @@ const Text = /*#__PURE__*/(() => { this.selectionEndIndex = 0; this.selectedText = null; - if(this.domContainer){ - this.domContainer.appendChild(this._domElSelectedText) - this.domContainer.appendChild(this._domElText) - }else{ - document.body.appendChild(this._domElSelectedText) - document.body.appendChild(this._domElText) - } + this.domContainer = this.domContainer ? this.domContainer : document.body + this.domContainer.appendChild(this._domElSelectedText) + this.domContainer.appendChild(this._domElText) + this.domContainer.style.position = 'relative' this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;' - this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' + this._domElText.style = 'position:absolute;left:-99px;opacity:0.5;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;background:green;' + this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0.5;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;background:blue;' this.startObservingMutation() @@ -826,11 +823,12 @@ const Text = /*#__PURE__*/(() => { * update the position of the overlaying HTML that contain all the text that need to be accessible to screen readers */ updateDomPosition(){ + let contbbox = this.domContainer.getBoundingClientRect() let bbox = this.renderer.domElement.getBoundingClientRect() let width = bbox.width let height = bbox.height - let left = bbox.left - let top = bbox.top + let left = bbox.left - contbbox.left + let top = bbox.top - contbbox.top var widthHalf = width / 2, heightHalf = height / 2; var max = new Vector3(0,0,0); @@ -895,11 +893,12 @@ const Text = /*#__PURE__*/(() => { return } + let contbbox = this.domContainer.getBoundingClientRect() let bbox = this.renderer.domElement.getBoundingClientRect() let width = bbox.width let height = bbox.height - let left = bbox.left - let top = bbox.top + let left = bbox.left - contbbox.left + let top = bbox.top - contbbox.top var widthHalf = width / 2, heightHalf = height / 2; var max = new Vector3(0,0,0); From 50d914c6222ed2e5d4a64ad9b9cf333cc903361c Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Fri, 5 Mar 2021 16:47:07 +0100 Subject: [PATCH 10/27] translation keep formating --- packages/troika-three-text/src/Text.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 3d814539..76080b55 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -107,8 +107,8 @@ const Text = /*#__PURE__*/(() => { this.domContainer.style.position = 'relative' this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElText.style = 'position:absolute;left:-99px;opacity:0.5;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;background:green;' - this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0.5;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;background:blue;' + this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;' + this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' this.startObservingMutation() @@ -134,6 +134,8 @@ const Text = /*#__PURE__*/(() => { this.text = '' this.prevText = '' this.currentText = '' + this.prevHTML = '' + this.currentHTML = '' /** * @deprecated Use `anchorX` and `anchorY` instead @@ -443,6 +445,8 @@ const Text = /*#__PURE__*/(() => { /* detect text change coming from the component */ if(this.prevText !== this.text){ this.currentText = this.text + this.prevHTML = this.currentHTML + this.currentHTML = this.text.replace(/(?:\r\n|\r|\n)/g, '
') this.selectionStartIndex = this.selectionEndIndex = -1 this.prevText = this.text } @@ -498,8 +502,11 @@ const Text = /*#__PURE__*/(() => { } //update dom with latest text - if(this._domElText.textContent !== this.currentText){ - this._domElText.textContent = this.currentText; + if(this.prevHTML !== this.currentHTML){ + this.observer.disconnect() + this._domElText.innerHTML = this.currentHTML; + this.prevHTML = this.currentHTML + this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); } this.dispatchEvent(syncCompleteEvent) @@ -1065,14 +1072,12 @@ const Text = /*#__PURE__*/(() => { * When a change occurs on the overlaying HTML, it reflect it in the renderer context */ mutationCallback(mutationsList, observer) { - if(this._domElText.textContent != this.currentText){ - this.currentText = this._domElText.textContent - console.log(this.currentText) + this.currentHTML = this._domElText.innerHTML + this.currentText = this.currentHTML.replace(/<(?!br\s*\/?)[^>]+>/g, '').replace(//gi, "\n"); this._needsSync = true; this.sync(()=>{ this.selectedText != '' ? this.updateSelection() : null }) - } } /** From 45d839aece65e9754284a0abc534362604f14fd3 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sat, 6 Mar 2021 23:04:16 +0100 Subject: [PATCH 11/27] module separation v0 --- .../src/facade/SelectionManagerFacade.js | 12 +- packages/troika-examples/text/TextExample.jsx | 17 +- .../troika-three-text/src/AccessibleText.js | 474 ++++++++++++++++++ packages/troika-three-text/src/Text.js | 404 +-------------- 4 files changed, 509 insertions(+), 398 deletions(-) create mode 100644 packages/troika-three-text/src/AccessibleText.js diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index 6aab803a..eb9be36b 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -48,7 +48,7 @@ class SelectionManagerFacade extends ListFacade { const textRenderInfo = textMesh.textRenderInfo if (textRenderInfo) { const textPos = textMesh.worldPositionToTextCoords(e.intersection.point, tempVec2) - const caret = textMesh.startCaret(textPos.x, textPos.y) + const caret = textMesh.a11yManager.startCaret(textRenderInfo,textPos.x, textPos.y) if (caret) { onSelectionChange(caret.charIndex, caret.charIndex) parent.addEventListener('drag', onDrag) @@ -75,7 +75,7 @@ class SelectionManagerFacade extends ListFacade { // textPos = ray.intersectPlane(tempPlane.setComponents(0, 0, 1, 0), tempVec3) } if (textPos) { - const caret = textMesh.moveCaret(textPos.x, textPos.y) + const caret = textMesh.a11yManager.moveCaret(textRenderInfo,textPos.x, textPos.y) if (caret) { onSelectionChange(this.selectionStart, caret.charIndex) } @@ -99,17 +99,17 @@ class SelectionManagerFacade extends ListFacade { target = target.parent } while (target !== null) //clear selection - textMesh.startCaret(0,0) + textMesh.a11yManager.clearSelection() }) parent.addEventListener('dragstart', onDragStart) parent.addEventListener('mousedown', onDragStart) var canvas = parent.getSceneFacade().parent._threeRenderer.domElement; canvas.addEventListener('contextmenu',(e)=>{ - textMesh._domElSelectedText.style.pointerEvents = 'auto' - textMesh.updateSelection() + textMesh.a11yManager._domElSelectedText.style.pointerEvents = 'auto' + textMesh.a11yManager.selectDomText() window.setTimeout(()=>{ - textMesh._domElSelectedText.style.pointerEvents = 'none' + textMesh.a11yManager._domElSelectedText.style.pointerEvents = 'none' console.log('contextmenu') },50) console.log('contextmenu') diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index f352279c..7ec3a15d 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -126,7 +126,11 @@ class TextExample extends React.Component { colorRanges: false, sdfGlyphSize: 6, debugSDF: false, - supportScreenReader: true + supportScreenReader: true, + camerax:0, + cameray:0, + cameraz:2, + lookAt:{x: 0, y: 0, z: 0} } this._onConfigUpdate = (newState) => { @@ -157,9 +161,10 @@ class TextExample extends React.Component { camera={ { fov: 75, aspect: width / height, - x: 0, - y: 0, - z: 2 + x: state.camerax, + y: state.cameray, + z: state.cameraz, + lookAt: state.lookAt } } lights={[ {type: 'ambient', color: 0x666666}, @@ -332,6 +337,10 @@ class TextExample extends React.Component { {type: 'number', path: "fillOpacity", min: 0, max: 1, step: 0.0001}, {type: 'number', path: "curveRadius", min: -5, max: 5, step: 0.001}, + {type: 'number', path: "camerax", min: -5, max: 5, step: 0.01}, + {type: 'number', path: "cameray", min: -5, max: 5, step: 0.01}, + {type: 'number', path: "cameraz", min: -5, max: 5, step: 0.01}, + {type: 'number', path: "outlineWidth", min: 0, max: 0.05, step: 0.0001}, {type: 'number', path: "outlineOpacity", min: 0, max: 1, step: 0.0001}, {type: 'number', path: "outlineOffsetX", min: -0.05, max: 0.05, step: 0.0001}, diff --git a/packages/troika-three-text/src/AccessibleText.js b/packages/troika-three-text/src/AccessibleText.js new file mode 100644 index 00000000..a1fc6b0a --- /dev/null +++ b/packages/troika-three-text/src/AccessibleText.js @@ -0,0 +1,474 @@ +import { + Mesh, + MeshBasicMaterial, + Vector4, + Vector3, + Vector2, + BoxBufferGeometry +} from 'three' +import { createDerivedMaterial } from 'troika-three-utils' +import { getSelectionRects, getCaretAtPoint } from './selectionUtils' + +const AccessibleText = /*#__PURE__*/(() => { + + const defaultSelectionColor = 0xffffff + + /** + * @class Text + * + * A ThreeJS Mesh that renders a string of text on a plane in 3D space using signed distance + * fields (SDF). + */ + class AccessibleText { + constructor(textMesh) { + + this.textMesh = textMesh + + this._domElSelectedText = document.createElement('p') + this._domElText = document.createElement(this.tagName ? this.tagName : 'p') + this.selectionStartIndex = 0; + this.selectionEndIndex = 0; + this.selectedText = null; + + this.domContainer = this.domContainer ? this.domContainer : document.body + this.domContainer.appendChild(this._domElSelectedText) + this.domContainer.appendChild(this._domElText) + this.domContainer.style.position = 'relative' + + this._domElSelectedText.setAttribute('aria-hidden','true') + this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;' + this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' + + this.startObservingMutation() + + this.selectionRects = [] + + this.prevText = '' + this.currentText = '' + this.prevHTML = '' + this.currentHTML = '' + this.prevCurveRadius = 0 + + /* create it only once */ + this.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) + /* create it only once */ + this.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1,32).translate(0.5, 0.5, 0.5) + + /** + * @member {THREE.Material} selectionMaterial + * Defines a _base_ material to be used when rendering the text. This material will be + * automatically replaced with a material derived from it, that adds shader code to + * decrease the alpha for each fragment (pixel) outside the text glyphs, with antialiasing. + * By default it will derive from a simple white MeshBasicMaterial, but you can use any + * of the other mesh materials to gain other features like lighting, texture maps, etc. + * + * Also see the `selectionColor` shortcut property. + */ + this.selectionMaterial = null + + /** + * @member {string|number|THREE.Color} selectionColor + * This is a shortcut for setting the `color` of the text's material. You can use this + * if you don't want to specify a whole custom `material`. Also, if you do use a custom + * `material`, this color will only be used for this particuar Text instance, even if + * that same material instance is shared across multiple Text objects. + */ + this.selectionColor = defaultSelectionColor + } + + + sync() { + if(this.prevText !== this.textMesh.text){ + this.textMesh.currentText = this.textMesh.text + this.prevHTML = this.currentHTML + this.currentHTML = this.textMesh.text.replace(/(?:\r\n|\r|\n)/g, '
') + this.prevText = this.textMesh.text + } + + this.textMesh.currentText = this.textMesh.currentText ? this.textMesh.currentText : this.textMesh.text + + //update dom with latest text + if(this.prevHTML !== this.currentHTML){ + this.observer.disconnect() + this._domElText.innerHTML = this.currentHTML; + this.prevHTML = this.currentHTML + this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); + } + } + + /** + * Given a local x/y coordinate in the text block plane, set the start position of the caret + * used in text selection + * @param {number} x + * @param {number} y + * @return {TextCaret | null} + */ + startCaret(textRenderInfo,x,y){ + let caret = getCaretAtPoint(textRenderInfo, x, y) + this.selectionStartIndex = caret.charIndex + this.selectionEndIndex = caret.charIndex + this.updateSelection(textRenderInfo) + return caret + } + + clearSelection(){ + this.selectionStartIndex = 0 + this.selectionEndIndex = 0 + this.selectionRects = [] + this._domElSelectedText.textContent = '' + this.highlightText() + this.updateSelectedDomPosition() + } + + /** + * Given a local x/y coordinate in the text block plane, set the end position of the caret + * used in text selection + * @param {number} x + * @param {number} y + * @return {TextCaret | null} + */ + moveCaret(textRenderInfo,x,y){ + let caret = getCaretAtPoint(textRenderInfo, x, y) + this.selectionEndIndex = caret.charIndex + this.updateSelection(textRenderInfo) + return caret + } + + /** + * update the selection visually and everything related to copy /paste + */ + updateSelection(textRenderInfo) { + this.selectedText = this.textMesh.text.substring(this.selectionStartIndex,this.selectionEndIndex) + this.selectionRects = getSelectionRects(textRenderInfo,this.selectionStartIndex,this.selectionEndIndex) + this._domElSelectedText.textContent = this.selectedText + this.highlightText() + this.selectDomText() + this.updateSelectedDomPosition() + } + + /** + * Select the text contened in _domElSelectedText in order for it to reflect what's currently selected in the Text + */ + selectDomText(){ + const sel = document.getSelection() + sel.removeAllRanges() + const range = document.createRange() + range.selectNodeContents(this._domElSelectedText); //sets Range + sel.removeAllRanges(); //remove all ranges from selection + sel.addRange(range); + } + + /** + * update the position of the overlaying HTML that contain all the text that need to be accessible to screen readers + */ + updateDomPosition(){ + let contbbox = this.domContainer.getBoundingClientRect() + let bbox = this.textMesh.renderer.domElement.getBoundingClientRect() + let width = bbox.width + let height = bbox.height + let left = bbox.left - contbbox.left + let top = bbox.top - contbbox.top + var widthHalf = width / 2, heightHalf = height / 2; + + var max = new Vector3(0,0,0); + var min = new Vector3(0,0,0); + + this.textMesh.geometry.computeBoundingBox() + max.copy(this.textMesh.geometry.boundingBox.max).applyMatrix4( this.textMesh.matrixWorld ); + min.copy(this.textMesh.geometry.boundingBox.min).applyMatrix4( this.textMesh.matrixWorld ); + + var bboxVectors = + [ + new Vector3(max.x,max.y,max.z), + new Vector3(min.x,max.y,max.z), + new Vector3(min.x,min.y,max.z), + new Vector3(max.x,min.y,max.z), + new Vector3(max.x,max.y,min.z), + new Vector3(min.x,max.y,min.z), + new Vector3(min.x,min.y,min.z), + new Vector3(max.x,min.y,min.z) + ] + + let xmin = null + let xmax = null + let ymin = null + let ymax = null + + + bboxVectors.forEach(vec => { + vec.project(this.textMesh.camera); + }); + xmin = bboxVectors[0].x + xmax = bboxVectors[0].x + ymin = bboxVectors[0].y + ymax = bboxVectors[0].y + bboxVectors.forEach(vec => { + xmin = xmin > vec.x ? vec.x : xmin + xmax = xmax < vec.x ? vec.x : xmax + ymin = ymin > vec.y ? vec.y : ymin + ymax = ymax < vec.y ? vec.y : ymax + }); + + xmax = ( xmax * widthHalf ) + widthHalf; + ymax = - ( ymax * heightHalf ) + heightHalf; + xmin = ( xmin * widthHalf ) + widthHalf; + ymin = - ( ymin * heightHalf ) + heightHalf; + + this._domElText.style.left = xmin+left+'px'; + this._domElText.style.top = ymax+top+'px'; + this._domElText.style.width = Math.abs(xmax-xmin)+'px'; + this._domElText.style.height = Math.abs(ymax-ymin)+'px'; + this._domElText.style.fontSize = Math.abs(ymax-ymin)+'px'; + } + + /** + * update the position of the overlaying HTML that contain + * the selected text in order for it to be acessible through context menu copy + */ + updateSelectedDomPosition(){ + if(this.textMesh.children.length === 0){ + this._domElSelectedText.style.width = '0px'; + this._domElSelectedText.style.height = '0px'; + return + } + + let contbbox = this.domContainer.getBoundingClientRect() + let bbox = this.textMesh.renderer.domElement.getBoundingClientRect() + let width = bbox.width + let height = bbox.height + let left = bbox.left - contbbox.left + let top = bbox.top - contbbox.top + var widthHalf = width / 2, heightHalf = height / 2; + + var max = new Vector3(0,0,0); + var min = new Vector3(0,0,0); + + + this.textMesh.children[0].geometry.computeBoundingBox() + this.textMesh.children[this.textMesh.children.length-1].geometry.computeBoundingBox() + + // max.copy(this.children[0].geometry.boundingBox.max) + // min.copy(this.children[0].geometry.boundingBox.min) + // max.max(this.children[this.children.length-1].geometry.boundingBox.max).applyMatrix4( this.children[this.children.length-1].matrixWorld ); + // min.min(this.children[this.children.length-1].geometry.boundingBox.min).applyMatrix4( this.children[this.children.length-1].matrixWorld ); + + let i=0; + for (let key in this.selectionRects) { + if(i===0){ + max.x = Math.max(this.selectionRects[key].left,this.selectionRects[key].right); + max.y = Math.max(this.selectionRects[key].top,this.selectionRects[key].bottom); + max.z = this.textMesh.geometry.boundingBox.max.z; + min.x = Math.min(this.selectionRects[key].left,this.selectionRects[key].right); + min.y = Math.min(this.selectionRects[key].top,this.selectionRects[key].bottom); + min.z = this.textMesh.geometry.boundingBox.min.z; + }else{ + max.x = Math.max(max.x,this.selectionRects[key].left,this.selectionRects[key].right); + max.y = Math.max(max.y,this.selectionRects[key].top,this.selectionRects[key].bottom); + min.x = Math.min(min.x,this.selectionRects[key].left,this.selectionRects[key].right); + min.y = Math.min(min.y,this.selectionRects[key].top,this.selectionRects[key].bottom); + } + i++; + } + + var bboxVectors = + [ + new Vector3(max.x,max.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), + new Vector3(min.x,max.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), + new Vector3(min.x,min.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), + new Vector3(max.x,min.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), + new Vector3(max.x,max.y,min.z).applyMatrix4( this.textMesh.matrixWorld ), + new Vector3(min.x,max.y,min.z).applyMatrix4( this.textMesh.matrixWorld ), + new Vector3(min.x,min.y,min.z).applyMatrix4( this.textMesh.matrixWorld ), + new Vector3(max.x,min.y,min.z).applyMatrix4( this.textMesh.matrixWorld ) + ] + + let xmin = null + let xmax = null + let ymin = null + let ymax = null + + bboxVectors.forEach(vec => { + vec.project(this.textMesh.camera); + }); + xmin = bboxVectors[0].x + xmax = bboxVectors[0].x + ymin = bboxVectors[0].y + ymax = bboxVectors[0].y + bboxVectors.forEach(vec => { + xmin = xmin > vec.x ? vec.x : xmin + xmax = xmax < vec.x ? vec.x : xmax + ymin = ymin > vec.y ? vec.y : ymin + ymax = ymax < vec.y ? vec.y : ymax + }); + + xmax = ( xmax * widthHalf ) + widthHalf; + ymax = - ( ymax * heightHalf ) + heightHalf; + xmin = ( xmin * widthHalf ) + widthHalf; + ymin = - ( ymin * heightHalf ) + heightHalf; + + this._domElSelectedText.style.left = xmin+left+'px'; + this._domElSelectedText.style.top = ymax+top+'px'; + this._domElSelectedText.style.width = Math.abs(xmax-xmin)+'px'; + this._domElSelectedText.style.height = Math.abs(ymax-ymin)+'px'; + } + + /** + * visually update the rendering of the text selection in the renderer context + */ + highlightText() { + + let THICKNESS = 0.25; + + //todo manage rect update in a cleaner way. Currently we recreate everything everytime + + //clean dispose of material no need to do it for geometry because we reuse the same + this.textMesh.children.forEach((child)=>{ + child.material.dispose() + }) + this.textMesh.children = [] + + for (let key in this.selectionRects) { + let material = createDerivedMaterial( + this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ + color:this.selectionColor ? this.selectionColor : defaultSelectionColor, + transparent: true, + opacity: 0.3, + depthWrite: false + }), + { + uniforms: { + rect: {value: new Vector4( + this.selectionRects[key].left , + this.selectionRects[key].top , + this.selectionRects[key].right , + this.selectionRects[key].bottom + )}, + depthAndCurveRadius: {value: new Vector2( + (this.selectionRects[key].top - this.selectionRects[key].bottom)*THICKNESS, + this.curveRadius + )} + }, + vertexDefs: ` + uniform vec4 rect; + uniform vec2 depthAndCurveRadius; + `, + vertexTransform: ` + float depth = depthAndCurveRadius.x; + float rad = depthAndCurveRadius.y; + position.x = mix(rect.x, rect.z, position.x); + position.y = mix(rect.w, rect.y, position.y); + position.z = mix(-depth * 0.5, depth * 0.5, position.z); + if (rad != 0.0) { + float angle = position.x / rad; + position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); + // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); + } + ` + } + ) + material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] + let selectRect = new Mesh( + this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, + material + // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) + ) + // selectRect.position.x = -1 + // selectRect.position.y = -1 + this.textMesh.add(selectRect) + } + this.textMesh.updateWorldMatrix(false,true) + } + + updateHighlightTextUniforms(){ + if( + this.prevCurveRadius === 0 && this.textMesh.curveRadius !== 0 + || + this.prevCurveRadius !== 0 && this.textMesh.curveRadius === 0 + ){ + this.prevCurveRadius = this.textMesh.curveRadius + //update geometry + for (let key in this.selectionRects) { + this.textMesh.children[key].geometry = this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry + } + } + for (let key in this.selectionRects) { + this.textMesh.children[key].material.uniforms.depthAndCurveRadius.value.y = this.textMesh.curveRadius + if(this.selectionColor != this.textMesh.children[key].material.color){ + //faster to check fo color change or to set needsUpdate true each time ? + //to discuss + this.textMesh.children[key].material.color.set(this.selectionColor) + this.textMesh.children[key].material.needsUpdate = true + } + } + } + + /** + * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context + */ + startObservingMutation(){ + //todo right now each Text class has its own MutationObserver, maybe it cn cause issues if used with multiple Text + this.observer = new MutationObserver(this.mutationCallback.bind(this)); + // Start observing the target node for change ( e.g. page translate ) + this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); + } + + /** + * When a change occurs on the overlaying HTML, it reflect it in the renderer context + */ + mutationCallback(mutationsList, observer) { + this.currentHTML = this._domElText.innerHTML + this.textMesh.currentText = this.currentHTML.replace(/<(?!br\s*\/?)[^>]+>/g, '').replace(//gi, "\n"); + this.clearSelection() + this.textMesh._needsSync = true; + this.textMesh.sync() + } + + /** + * stop monitoring dom change + */ + stopObservingMutation(){ + this.observer.disconnect(); + } + + /** + * @override Custom raycasting to test against the whole text block's max rectangular bounds + * TODO is there any reason to make this more granular, like within individual line or glyph rects? + */ + raycast(raycaster, intersects) { + const {textRenderInfo, curveRadius} = this + if (textRenderInfo) { + const bounds = textRenderInfo.blockBounds + const raycastMesh = curveRadius ? getCurvedRaycastMesh() : getFlatRaycastMesh() + const geom = raycastMesh.geometry + const {position, uv} = geom.attributes + for (let i = 0; i < uv.count; i++) { + let x = bounds[0] + (uv.getX(i) * (bounds[2] - bounds[0])) + const y = bounds[1] + (uv.getY(i) * (bounds[3] - bounds[1])) + let z = 0 + if (curveRadius) { + z = curveRadius - Math.cos(x / curveRadius) * curveRadius + x = Math.sin(x / curveRadius) * curveRadius + } + position.setXYZ(i, x, y, z) + } + geom.boundingSphere = this.geometry.boundingSphere + geom.boundingBox = this.geometry.boundingBox + raycastMesh.matrixWorld = this.matrixWorld + raycastMesh.material.side = this.material.side + tempArray.length = 0 + raycastMesh.raycast(raycaster, tempArray) + for (let i = 0; i < tempArray.length; i++) { + tempArray[i].object = this + intersects.push(tempArray[i]) + } + } + } + + } + + return AccessibleText +})() + +export { + AccessibleText +} diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 76080b55..48307875 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -5,16 +5,13 @@ import { Mesh, MeshBasicMaterial, PlaneBufferGeometry, - Vector4, Vector3, Vector2, - BoxBufferGeometry } from 'three' import { GlyphsGeometry } from './GlyphsGeometry.js' import { createTextDerivedMaterial } from './TextDerivedMaterial.js' import { getTextRenderInfo } from './TextBuilder.js' -import { createDerivedMaterial } from 'troika-three-utils' -import { getSelectionRects, getCaretAtPoint } from './selectionUtils' +import { AccessibleText } from './AccessibleText' const Text = /*#__PURE__*/(() => { @@ -25,7 +22,6 @@ const Text = /*#__PURE__*/(() => { transparent: true }) const defaultStrokeColor = 0x808080 - const defaultSelectionColor = 0xffffff const tempMat4 = new Matrix4() const tempVec3a = new Vector3() @@ -95,31 +91,6 @@ const Text = /*#__PURE__*/(() => { class Text extends Mesh { constructor() { - this._domElSelectedText = document.createElement('p') - this._domElText = document.createElement(this.tagName ? this.tagName : 'p') - this.selectionStartIndex = 0; - this.selectionEndIndex = 0; - this.selectedText = null; - - this.domContainer = this.domContainer ? this.domContainer : document.body - this.domContainer.appendChild(this._domElSelectedText) - this.domContainer.appendChild(this._domElText) - this.domContainer.style.position = 'relative' - - this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;' - this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' - - this.startObservingMutation() - - this.selectionRects = [] - - //TODO test html support - - //syncing html on top of text can slow down the page if used with multiple Text instance - //sometime the text is purely decorative and it makes no sense for it to be accessible, so it should be possible to disable / enable it - //the default is to be discussed - this.supportScreenReader = false this.selectable = false const geometry = new GlyphsGeometry() @@ -127,15 +98,14 @@ const Text = /*#__PURE__*/(() => { // === Text layout properties: === // + this.a11yManager = new AccessibleText(this) + /** * @member {string} text * The string of text to be rendered. */ this.text = '' - this.prevText = '' this.currentText = '' - this.prevHTML = '' - this.currentHTML = '' /** * @deprecated Use `anchorX` and `anchorY` instead @@ -278,15 +248,6 @@ const Text = /*#__PURE__*/(() => { */ this.color = null - /** - * @member {string|number|THREE.Color} selectionColor - * This is a shortcut for setting the `color` of the text's material. You can use this - * if you don't want to specify a whole custom `material`. Also, if you do use a custom - * `material`, this color will only be used for this particuar Text instance, even if - * that same material instance is shared across multiple Text objects. - */ - this.selectionColor = defaultSelectionColor - /** * @member {object|null} colorRanges * WARNING: This API is experimental and may change. @@ -442,17 +403,6 @@ const Text = /*#__PURE__*/(() => { if (this._needsSync) { this._needsSync = false - /* detect text change coming from the component */ - if(this.prevText !== this.text){ - this.currentText = this.text - this.prevHTML = this.currentHTML - this.currentHTML = this.text.replace(/(?:\r\n|\r|\n)/g, '
') - this.selectionStartIndex = this.selectionEndIndex = -1 - this.prevText = this.text - } - - this.currentText = this.currentText ? this.currentText : this.text - // If there's another sync still in progress, queue if (this._isSyncing) { (this._queuedSyncs || (this._queuedSyncs = [])).push(callback) @@ -460,6 +410,12 @@ const Text = /*#__PURE__*/(() => { this._isSyncing = true this.dispatchEvent(syncStartEvent) + if(this.a11yManager){ + this.a11yManager.sync() + } + + this.currentText = this.currentText ? this.currentText : this.text + getTextRenderInfo({ text: this.currentText, font: this.font, @@ -501,14 +457,6 @@ const Text = /*#__PURE__*/(() => { }) } - //update dom with latest text - if(this.prevHTML !== this.currentHTML){ - this.observer.disconnect() - this._domElText.innerHTML = this.currentHTML; - this.prevHTML = this.currentHTML - this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); - } - this.dispatchEvent(syncCompleteEvent) if (callback) { callback() @@ -517,13 +465,11 @@ const Text = /*#__PURE__*/(() => { } } } - + onAfterRender(){ - if( this.supportScreenReader ){ - this.updateDomPosition() - } - if( this.selectable ){ - this.updateSelectedDomPosition() + if(this.a11yManager){ + this.a11yManager.updateDomPosition() + this.a11yManager.updateSelectedDomPosition() } } @@ -536,10 +482,11 @@ const Text = /*#__PURE__*/(() => { onBeforeRender(renderer, scene, camera, geometry, material, group) { this.camera = camera this.renderer = renderer + this.sync() - if( this.selectable ){ - this.updateHighlightTextUniforms() + if(this.a11yManager){ + this.a11yManager.updateHighlightTextUniforms() } // This may not always be a text material, e.g. if there's a scene.overrideMaterial present @@ -768,325 +715,6 @@ const Text = /*#__PURE__*/(() => { return this.localPositionToTextCoords(this.worldToLocal(tempVec3a), target) } - /** - * Given a local x/y coordinate in the text block plane, set the start position of the caret - * used in text selection - * @param {number} x - * @param {number} y - * @return {TextCaret | null} - */ - startCaret(x,y){ - let caret = getCaretAtPoint(this.textRenderInfo, x, y) - this.selectionStartIndex = caret.charIndex - this.selectionEndIndex = caret.charIndex - this.updateSelection() - return caret - } - - /** - * Given a local x/y coordinate in the text block plane, set the end position of the caret - * used in text selection - * @param {number} x - * @param {number} y - * @return {TextCaret | null} - */ - moveCaret(x,y){ - let caret = getCaretAtPoint(this.textRenderInfo, x, y) - this.selectionEndIndex = caret.charIndex - this.updateSelection() - return caret - } - - /** - * update the selection visually and everything related to copy /paste - */ - updateSelection() { - if(this.selectable){ - this.selectedText = this.text.substring(this.selectionStartIndex,this.selectionEndIndex) - this.selectionRects = getSelectionRects(this._textRenderInfo,this.selectionStartIndex,this.selectionEndIndex) - this._domElSelectedText.textContent = this.selectedText - this.selectDomText() - this.updateSelectedDomPosition() - }else{ - this.selectedText = null - this.selectionRects = [] - } - } - - /** - * Select the text contened in _domElSelectedText in order for it to reflect what's currently selected in the Text - */ - selectDomText(){ - this.highlightText() - const sel = document.getSelection() - sel.removeAllRanges() - const range = document.createRange() - range.selectNodeContents(this._domElSelectedText); //sets Range - sel.removeAllRanges(); //remove all ranges from selection - sel.addRange(range); - } - - /** - * update the position of the overlaying HTML that contain all the text that need to be accessible to screen readers - */ - updateDomPosition(){ - let contbbox = this.domContainer.getBoundingClientRect() - let bbox = this.renderer.domElement.getBoundingClientRect() - let width = bbox.width - let height = bbox.height - let left = bbox.left - contbbox.left - let top = bbox.top - contbbox.top - var widthHalf = width / 2, heightHalf = height / 2; - - var max = new Vector3(0,0,0); - var min = new Vector3(0,0,0); - - this.geometry.computeBoundingBox() - max.copy(this.geometry.boundingBox.max).applyMatrix4( this.matrixWorld ); - min.copy(this.geometry.boundingBox.min).applyMatrix4( this.matrixWorld ); - - var bboxVectors = - [ - new Vector3(max.x,max.y,max.z), - new Vector3(min.x,max.y,max.z), - new Vector3(min.x,min.y,max.z), - new Vector3(max.x,min.y,max.z), - new Vector3(max.x,max.y,min.z), - new Vector3(min.x,max.y,min.z), - new Vector3(min.x,min.y,min.z), - new Vector3(max.x,min.y,min.z) - ] - - let xmin = null - let xmax = null - let ymin = null - let ymax = null - - - bboxVectors.forEach(vec => { - vec.project(this.camera); - }); - xmin = bboxVectors[0].x - xmax = bboxVectors[0].x - ymin = bboxVectors[0].y - ymax = bboxVectors[0].y - bboxVectors.forEach(vec => { - xmin = xmin > vec.x ? vec.x : xmin - xmax = xmax < vec.x ? vec.x : xmax - ymin = ymin > vec.y ? vec.y : ymin - ymax = ymax < vec.y ? vec.y : ymax - }); - - xmax = ( xmax * widthHalf ) + widthHalf; - ymax = - ( ymax * heightHalf ) + heightHalf; - xmin = ( xmin * widthHalf ) + widthHalf; - ymin = - ( ymin * heightHalf ) + heightHalf; - - this._domElText.style.left = xmin+left+'px'; - this._domElText.style.top = ymax+top+'px'; - this._domElText.style.width = Math.abs(xmax-xmin)+'px'; - this._domElText.style.height = Math.abs(ymax-ymin)+'px'; - this._domElText.style.fontSize = Math.abs(ymax-ymin)+'px'; - } - - /** - * update the position of the overlaying HTML that contain - * the selected text in order for it to be acessible through context menu copy - */ - updateSelectedDomPosition(){ - if(this.children.length === 0){ - this._domElSelectedText.style.width = '0px'; - this._domElSelectedText.style.height = '0px'; - return - } - - let contbbox = this.domContainer.getBoundingClientRect() - let bbox = this.renderer.domElement.getBoundingClientRect() - let width = bbox.width - let height = bbox.height - let left = bbox.left - contbbox.left - let top = bbox.top - contbbox.top - var widthHalf = width / 2, heightHalf = height / 2; - - var max = new Vector3(0,0,0); - var min = new Vector3(0,0,0); - - - this.children[0].geometry.computeBoundingBox() - this.children[this.children.length-1].geometry.computeBoundingBox() - - // max.copy(this.children[0].geometry.boundingBox.max) - // min.copy(this.children[0].geometry.boundingBox.min) - // max.max(this.children[this.children.length-1].geometry.boundingBox.max).applyMatrix4( this.children[this.children.length-1].matrixWorld ); - // min.min(this.children[this.children.length-1].geometry.boundingBox.min).applyMatrix4( this.children[this.children.length-1].matrixWorld ); - - let i=0; - for (let key in this.selectionRects) { - if(i===0){ - max.x = Math.max(this.selectionRects[key].left,this.selectionRects[key].right); - max.y = Math.max(this.selectionRects[key].top,this.selectionRects[key].bottom); - max.z = this.geometry.boundingBox.max.z; - min.x = Math.min(this.selectionRects[key].left,this.selectionRects[key].right); - min.y = Math.min(this.selectionRects[key].top,this.selectionRects[key].bottom); - min.z = this.geometry.boundingBox.min.z; - }else{ - max.x = Math.max(max.x,this.selectionRects[key].left,this.selectionRects[key].right); - max.y = Math.max(max.y,this.selectionRects[key].top,this.selectionRects[key].bottom); - min.x = Math.min(min.x,this.selectionRects[key].left,this.selectionRects[key].right); - min.y = Math.min(min.y,this.selectionRects[key].top,this.selectionRects[key].bottom); - } - i++; - } - - var bboxVectors = - [ - new Vector3(max.x,max.y,max.z).applyMatrix4( this.matrixWorld ), - new Vector3(min.x,max.y,max.z).applyMatrix4( this.matrixWorld ), - new Vector3(min.x,min.y,max.z).applyMatrix4( this.matrixWorld ), - new Vector3(max.x,min.y,max.z).applyMatrix4( this.matrixWorld ), - new Vector3(max.x,max.y,min.z).applyMatrix4( this.matrixWorld ), - new Vector3(min.x,max.y,min.z).applyMatrix4( this.matrixWorld ), - new Vector3(min.x,min.y,min.z).applyMatrix4( this.matrixWorld ), - new Vector3(max.x,min.y,min.z).applyMatrix4( this.matrixWorld ) - ] - - let xmin = null - let xmax = null - let ymin = null - let ymax = null - - bboxVectors.forEach(vec => { - vec.project(this.camera); - }); - xmin = bboxVectors[0].x - xmax = bboxVectors[0].x - ymin = bboxVectors[0].y - ymax = bboxVectors[0].y - bboxVectors.forEach(vec => { - xmin = xmin > vec.x ? vec.x : xmin - xmax = xmax < vec.x ? vec.x : xmax - ymin = ymin > vec.y ? vec.y : ymin - ymax = ymax < vec.y ? vec.y : ymax - }); - - xmax = ( xmax * widthHalf ) + widthHalf; - ymax = - ( ymax * heightHalf ) + heightHalf; - xmin = ( xmin * widthHalf ) + widthHalf; - ymin = - ( ymin * heightHalf ) + heightHalf; - - this._domElSelectedText.style.left = xmin+left+'px'; - this._domElSelectedText.style.top = ymax+top+'px'; - this._domElSelectedText.style.width = Math.abs(xmax-xmin)+'px'; - this._domElSelectedText.style.height = Math.abs(ymax-ymin)+'px'; - } - - /** - * visually update the rendering of the text selection in the renderer context - */ - highlightText() { - - let THICKNESS = 0.25; - - //todo manage rect update in a cleaner way. Currently we recreate everything everytime - this.children = [] - - for (let key in this.selectionRects) { - let material = createDerivedMaterial( - this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ - color:this.selectionColor ? this.selectionColor : defaultSelectionColor, - transparent: true, - opacity: 0.3, - depthWrite: false - }), - { - uniforms: { - rect: {value: new Vector4( - this.selectionRects[key].left , - this.selectionRects[key].top , - this.selectionRects[key].right , - this.selectionRects[key].bottom - )}, - depthAndCurveRadius: {value: new Vector2( - (this.selectionRects[key].top - this.selectionRects[key].bottom)*THICKNESS, - this.curveRadius - )} - }, - vertexDefs: ` - uniform vec4 rect; - uniform vec2 depthAndCurveRadius; - `, - vertexTransform: ` - float depth = depthAndCurveRadius.x; - float rad = depthAndCurveRadius.y; - position.x = mix(rect.x, rect.z, position.x); - position.y = mix(rect.w, rect.y, position.y); - position.z = mix(-depth * 0.5, depth * 0.5, position.z); - if (rad != 0.0) { - float angle = position.x / rad; - position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); - // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); - } - ` - } - ) - material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] - let selectRect = new Mesh( - new BoxBufferGeometry(1, 1, 0.1, 32).translate(0.5, 0.5, 0.5), - material - // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) - ) - // selectRect.position.x = -1 - // selectRect.position.y = -1 - this.add(selectRect) - } - this.updateWorldMatrix(false,true) - } - - updateHighlightTextUniforms(){ - for (let key in this.selectionRects) { - this.children[key].material.uniforms.depthAndCurveRadius.value.y = this.curveRadius - this.children[key].material.uniforms.rect.value.x = this.selectionRects[key].left - this.children[key].material.uniforms.rect.value.y = this.selectionRects[key].top - this.children[key].material.uniforms.rect.value.z = this.selectionRects[key].right - this.children[key].material.uniforms.rect.value.w = this.selectionRects[key].bottom - if(this.selectionColor != this.children[key].material.color){ - //faster to check fo color change or to set needsUpdate true each time ? - //to discuss - this.children[key].material.color.set(this.selectionColor) - this.children[key].material.needsUpdate = true - } - } - } - - /** - * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context - */ - startObservingMutation(){ - //todo right now each Text class has its own MutationObserver, maybe it cn cause issues if used with multiple Text - this.observer = new MutationObserver(this.mutationCallback.bind(this)); - // Start observing the target node for change ( e.g. page translate ) - this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); - } - - /** - * When a change occurs on the overlaying HTML, it reflect it in the renderer context - */ - mutationCallback(mutationsList, observer) { - this.currentHTML = this._domElText.innerHTML - this.currentText = this.currentHTML.replace(/<(?!br\s*\/?)[^>]+>/g, '').replace(//gi, "\n"); - this._needsSync = true; - this.sync(()=>{ - this.selectedText != '' ? this.updateSelection() : null - }) - } - - /** - * stop monitoring dom change - */ - stopObservingMutation(){ - this.observer.disconnect(); - } - /** * @override Custom raycasting to test against the whole text block's max rectangular bounds * TODO is there any reason to make this more granular, like within individual line or glyph rects? From d58db14386ab4023a362831e380fed9876a50a80 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Sat, 6 Mar 2021 21:57:04 -0700 Subject: [PATCH 12/27] use CSS matrix3d for aligning DOM overlays more accurately with perspective --- .../troika-three-text/src/AccessibleText.js | 210 ++++++------------ packages/troika-three-text/src/Text.js | 22 +- 2 files changed, 78 insertions(+), 154 deletions(-) diff --git a/packages/troika-three-text/src/AccessibleText.js b/packages/troika-three-text/src/AccessibleText.js index a1fc6b0a..5fe066ad 100644 --- a/packages/troika-three-text/src/AccessibleText.js +++ b/packages/troika-three-text/src/AccessibleText.js @@ -1,4 +1,5 @@ import { + Matrix4, Mesh, MeshBasicMaterial, Vector4, @@ -9,10 +10,29 @@ import { import { createDerivedMaterial } from 'troika-three-utils' import { getSelectionRects, getCaretAtPoint } from './selectionUtils' +const domOverlayBaseStyles = ` +position:fixed; +top:0; +left:0; +opacity:0; +overflow:hidden; +margin:0; +pointer-events:none; +width:10px; +height:10px; +transform-origin:0 0; +font-size:10px; +line-height: 10px; +` + const AccessibleText = /*#__PURE__*/(() => { const defaultSelectionColor = 0xffffff + const tempMat4a = new Matrix4() + const tempMat4b = new Matrix4() + const tempVec3 = new Vector3() + /** * @class Text * @@ -30,14 +50,12 @@ const AccessibleText = /*#__PURE__*/(() => { this.selectionEndIndex = 0; this.selectedText = null; - this.domContainer = this.domContainer ? this.domContainer : document.body + this.domContainer = this.domContainer ? this.domContainer : document.documentElement this.domContainer.appendChild(this._domElSelectedText) this.domContainer.appendChild(this._domElText) - this.domContainer.style.position = 'relative' this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;display:flex;align-items: center;line-height: 0px!important;line-break: anywhere;' - this._domElSelectedText.style = 'position:absolute;left:-99px;opacity:0;overflow:hidden;margin:0px;pointer-events:none;font-size:100vh;' + this._domElText.style = this._domElSelectedText.style = domOverlayBaseStyles this.startObservingMutation() @@ -117,7 +135,6 @@ const AccessibleText = /*#__PURE__*/(() => { this.selectionRects = [] this._domElSelectedText.textContent = '' this.highlightText() - this.updateSelectedDomPosition() } /** @@ -143,7 +160,6 @@ const AccessibleText = /*#__PURE__*/(() => { this._domElSelectedText.textContent = this.selectedText this.highlightText() this.selectDomText() - this.updateSelectedDomPosition() } /** @@ -161,155 +177,65 @@ const AccessibleText = /*#__PURE__*/(() => { /** * update the position of the overlaying HTML that contain all the text that need to be accessible to screen readers */ - updateDomPosition(){ - let contbbox = this.domContainer.getBoundingClientRect() - let bbox = this.textMesh.renderer.domElement.getBoundingClientRect() - let width = bbox.width - let height = bbox.height - let left = bbox.left - contbbox.left - let top = bbox.top - contbbox.top - var widthHalf = width / 2, heightHalf = height / 2; - - var max = new Vector3(0,0,0); - var min = new Vector3(0,0,0); - - this.textMesh.geometry.computeBoundingBox() - max.copy(this.textMesh.geometry.boundingBox.max).applyMatrix4( this.textMesh.matrixWorld ); - min.copy(this.textMesh.geometry.boundingBox.min).applyMatrix4( this.textMesh.matrixWorld ); - - var bboxVectors = - [ - new Vector3(max.x,max.y,max.z), - new Vector3(min.x,max.y,max.z), - new Vector3(min.x,min.y,max.z), - new Vector3(max.x,min.y,max.z), - new Vector3(max.x,max.y,min.z), - new Vector3(min.x,max.y,min.z), - new Vector3(min.x,min.y,min.z), - new Vector3(max.x,min.y,min.z) - ] - - let xmin = null - let xmax = null - let ymin = null - let ymax = null - - - bboxVectors.forEach(vec => { - vec.project(this.textMesh.camera); - }); - xmin = bboxVectors[0].x - xmax = bboxVectors[0].x - ymin = bboxVectors[0].y - ymax = bboxVectors[0].y - bboxVectors.forEach(vec => { - xmin = xmin > vec.x ? vec.x : xmin - xmax = xmax < vec.x ? vec.x : xmax - ymin = ymin > vec.y ? vec.y : ymin - ymax = ymax < vec.y ? vec.y : ymax - }); - - xmax = ( xmax * widthHalf ) + widthHalf; - ymax = - ( ymax * heightHalf ) + heightHalf; - xmin = ( xmin * widthHalf ) + widthHalf; - ymin = - ( ymin * heightHalf ) + heightHalf; - - this._domElText.style.left = xmin+left+'px'; - this._domElText.style.top = ymax+top+'px'; - this._domElText.style.width = Math.abs(xmax-xmin)+'px'; - this._domElText.style.height = Math.abs(ymax-ymin)+'px'; - this._domElText.style.fontSize = Math.abs(ymax-ymin)+'px'; + updateDomPosition(renderer, camera) { + const {min, max} = this.textMesh.geometry.boundingBox + this._domElText.style.transform = this._textRectToCssMatrix(min.x, min.y, max.x, max.y, max.z, renderer, camera) } /** * update the position of the overlaying HTML that contain * the selected text in order for it to be acessible through context menu copy */ - updateSelectedDomPosition(){ - if(this.textMesh.children.length === 0){ - this._domElSelectedText.style.width = '0px'; - this._domElSelectedText.style.height = '0px'; - return + updateSelectedDomPosition(renderer, camera) { + const rects = this.selectionRects + const el = this._domElSelectedText + if (rects && rects.length) { + // Find local space rect containing all selection rects + // TODO can we wrap this even tighter to multiline selections where top/bottom lines are partially selected? + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity + for (let i = 0; i < rects.length; i++) { + minX = Math.min(minX, rects[i].left) + minY = Math.min(minY, rects[i].bottom) + maxX = Math.max(maxX, rects[i].right) + maxY = Math.max(maxY, rects[i].top) + } + + const z = this.textMesh.geometry.boundingBox.max.z + el.style.transform = this._textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera) + el.style.display = 'block' + } else { + el.style.display = 'none' } - - let contbbox = this.domContainer.getBoundingClientRect() - let bbox = this.textMesh.renderer.domElement.getBoundingClientRect() - let width = bbox.width - let height = bbox.height - let left = bbox.left - contbbox.left - let top = bbox.top - contbbox.top - var widthHalf = width / 2, heightHalf = height / 2; + } - var max = new Vector3(0,0,0); - var min = new Vector3(0,0,0); + /** + * Given a rect in local text coordinates, build a CSS matrix3d that will transform + * a 10x10 DOM element to line up exactly with that rect on the screen. + * @private + */ + _textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera) { + const canvasRect = renderer.domElement.getBoundingClientRect() + // element dimensions to geometry dimensions (flipping the y) + tempMat4a.makeScale((maxX - minX) / 10, (minY - maxY) / 10, 1) + .setPosition(tempVec3.set(minX, maxY, z)) - this.textMesh.children[0].geometry.computeBoundingBox() - this.textMesh.children[this.textMesh.children.length-1].geometry.computeBoundingBox() + // geometry to world + tempMat4a.premultiply(this.textMesh.matrixWorld) - // max.copy(this.children[0].geometry.boundingBox.max) - // min.copy(this.children[0].geometry.boundingBox.min) - // max.max(this.children[this.children.length-1].geometry.boundingBox.max).applyMatrix4( this.children[this.children.length-1].matrixWorld ); - // min.min(this.children[this.children.length-1].geometry.boundingBox.min).applyMatrix4( this.children[this.children.length-1].matrixWorld ); + // world to camera + tempMat4a.premultiply(camera.matrixWorldInverse) - let i=0; - for (let key in this.selectionRects) { - if(i===0){ - max.x = Math.max(this.selectionRects[key].left,this.selectionRects[key].right); - max.y = Math.max(this.selectionRects[key].top,this.selectionRects[key].bottom); - max.z = this.textMesh.geometry.boundingBox.max.z; - min.x = Math.min(this.selectionRects[key].left,this.selectionRects[key].right); - min.y = Math.min(this.selectionRects[key].top,this.selectionRects[key].bottom); - min.z = this.textMesh.geometry.boundingBox.min.z; - }else{ - max.x = Math.max(max.x,this.selectionRects[key].left,this.selectionRects[key].right); - max.y = Math.max(max.y,this.selectionRects[key].top,this.selectionRects[key].bottom); - min.x = Math.min(min.x,this.selectionRects[key].left,this.selectionRects[key].right); - min.y = Math.min(min.y,this.selectionRects[key].top,this.selectionRects[key].bottom); - } - i++; - } + // camera to projection + tempMat4a.premultiply(camera.projectionMatrix) + + // projection coords (-1 to 1) to screen pixels + tempMat4a.premultiply( + tempMat4b.makeScale(canvasRect.width / 2, -canvasRect.height / 2, 1) + .setPosition(canvasRect.left + canvasRect.width / 2, canvasRect.top + canvasRect.height / 2, 0) + ) - var bboxVectors = - [ - new Vector3(max.x,max.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), - new Vector3(min.x,max.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), - new Vector3(min.x,min.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), - new Vector3(max.x,min.y,max.z).applyMatrix4( this.textMesh.matrixWorld ), - new Vector3(max.x,max.y,min.z).applyMatrix4( this.textMesh.matrixWorld ), - new Vector3(min.x,max.y,min.z).applyMatrix4( this.textMesh.matrixWorld ), - new Vector3(min.x,min.y,min.z).applyMatrix4( this.textMesh.matrixWorld ), - new Vector3(max.x,min.y,min.z).applyMatrix4( this.textMesh.matrixWorld ) - ] - - let xmin = null - let xmax = null - let ymin = null - let ymax = null - - bboxVectors.forEach(vec => { - vec.project(this.textMesh.camera); - }); - xmin = bboxVectors[0].x - xmax = bboxVectors[0].x - ymin = bboxVectors[0].y - ymax = bboxVectors[0].y - bboxVectors.forEach(vec => { - xmin = xmin > vec.x ? vec.x : xmin - xmax = xmax < vec.x ? vec.x : xmax - ymin = ymin > vec.y ? vec.y : ymin - ymax = ymax < vec.y ? vec.y : ymax - }); - - xmax = ( xmax * widthHalf ) + widthHalf; - ymax = - ( ymax * heightHalf ) + heightHalf; - xmin = ( xmin * widthHalf ) + widthHalf; - ymin = - ( ymin * heightHalf ) + heightHalf; - - this._domElSelectedText.style.left = xmin+left+'px'; - this._domElSelectedText.style.top = ymax+top+'px'; - this._domElSelectedText.style.width = Math.abs(xmax-xmin)+'px'; - this._domElSelectedText.style.height = Math.abs(ymax-ymin)+'px'; + return `matrix3d(${tempMat4a.elements.join(',')})` } /** diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 48307875..69fbd015 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -90,14 +90,12 @@ const Text = /*#__PURE__*/(() => { */ class Text extends Mesh { constructor() { - - this.selectable = false - const geometry = new GlyphsGeometry() super(geometry, null) // === Text layout properties: === // + this.selectable = false this.a11yManager = new AccessibleText(this) /** @@ -465,11 +463,11 @@ const Text = /*#__PURE__*/(() => { } } } - - onAfterRender(){ + + onAfterRender(renderer, scene, camera) { if(this.a11yManager){ - this.a11yManager.updateDomPosition() - this.a11yManager.updateSelectedDomPosition() + this.a11yManager.updateDomPosition(renderer, camera) + this.a11yManager.updateSelectedDomPosition(renderer, camera) } } @@ -572,11 +570,11 @@ const Text = /*#__PURE__*/(() => { this.geometry.detail = detail } - get curveRadius() { - return this.geometry.curveRadius - } - set curveRadius(r) { - this.geometry.curveRadius = r + get curveRadius() { + return this.geometry.curveRadius + } + set curveRadius(r) { + this.geometry.curveRadius = r } // Create and update material for shadows upon request: From 7d6bae2b634c18c48212fa0283e1e929abfc2580 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Sat, 6 Mar 2021 21:57:04 -0700 Subject: [PATCH 13/27] cherry picked + SR outline --- packages/troika-three-text/src/AccessibleText.js | 13 ++++++++++--- packages/troika-three-text/src/Text.js | 4 +--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/troika-three-text/src/AccessibleText.js b/packages/troika-three-text/src/AccessibleText.js index 5fe066ad..c767493f 100644 --- a/packages/troika-three-text/src/AccessibleText.js +++ b/packages/troika-three-text/src/AccessibleText.js @@ -25,6 +25,13 @@ font-size:10px; line-height: 10px; ` +const domSRoutline = ` + line-break: anywhere; + line-height: 0px; + display: flex; + align-items: center; +` + const AccessibleText = /*#__PURE__*/(() => { const defaultSelectionColor = 0xffffff @@ -50,12 +57,14 @@ const AccessibleText = /*#__PURE__*/(() => { this.selectionEndIndex = 0; this.selectedText = null; + //todo how to pass a dom container this.domContainer = this.domContainer ? this.domContainer : document.documentElement this.domContainer.appendChild(this._domElSelectedText) this.domContainer.appendChild(this._domElText) this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElText.style = this._domElSelectedText.style = domOverlayBaseStyles + this._domElSelectedText.style = domOverlayBaseStyles + this._domElText.style = domOverlayBaseStyles + domSRoutline this.startObservingMutation() @@ -332,9 +341,7 @@ const AccessibleText = /*#__PURE__*/(() => { * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context */ startObservingMutation(){ - //todo right now each Text class has its own MutationObserver, maybe it cn cause issues if used with multiple Text this.observer = new MutationObserver(this.mutationCallback.bind(this)); - // Start observing the target node for change ( e.g. page translate ) this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); } diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 69fbd015..f537f19b 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -96,6 +96,7 @@ const Text = /*#__PURE__*/(() => { // === Text layout properties: === // this.selectable = false + this.domContainer = null this.a11yManager = new AccessibleText(this) /** @@ -478,9 +479,6 @@ const Text = /*#__PURE__*/(() => { * @override */ onBeforeRender(renderer, scene, camera, geometry, material, group) { - this.camera = camera - this.renderer = renderer - this.sync() if(this.a11yManager){ From 42053ac7a76e372fe17762135475dd3571479fef Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sun, 7 Mar 2021 18:32:51 +0100 Subject: [PATCH 14/27] manage selection without deleting potential Text childs --- .../troika-three-text/src/AccessibleText.js | 169 +++++++----------- 1 file changed, 68 insertions(+), 101 deletions(-) diff --git a/packages/troika-three-text/src/AccessibleText.js b/packages/troika-three-text/src/AccessibleText.js index c767493f..c3ca60e7 100644 --- a/packages/troika-three-text/src/AccessibleText.js +++ b/packages/troika-three-text/src/AccessibleText.js @@ -69,6 +69,7 @@ const AccessibleText = /*#__PURE__*/(() => { this.startObservingMutation() this.selectionRects = [] + this.selectionRectsMeshs = [] this.prevText = '' this.currentText = '' @@ -255,62 +256,61 @@ const AccessibleText = /*#__PURE__*/(() => { let THICKNESS = 0.25; //todo manage rect update in a cleaner way. Currently we recreate everything everytime - //clean dispose of material no need to do it for geometry because we reuse the same - this.textMesh.children.forEach((child)=>{ - child.material.dispose() + this.selectionRectsMeshs.forEach((rect)=>{ + rect.parent.remove(rect) + rect.material.dispose() }) - this.textMesh.children = [] + this.selectionRectsMeshs=[] - for (let key in this.selectionRects) { + this.selectionRects.forEach((rect)=>{ let material = createDerivedMaterial( - this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ - color:this.selectionColor ? this.selectionColor : defaultSelectionColor, - transparent: true, - opacity: 0.3, - depthWrite: false - }), - { - uniforms: { - rect: {value: new Vector4( - this.selectionRects[key].left , - this.selectionRects[key].top , - this.selectionRects[key].right , - this.selectionRects[key].bottom - )}, - depthAndCurveRadius: {value: new Vector2( - (this.selectionRects[key].top - this.selectionRects[key].bottom)*THICKNESS, - this.curveRadius - )} - }, - vertexDefs: ` - uniform vec4 rect; - uniform vec2 depthAndCurveRadius; - `, - vertexTransform: ` - float depth = depthAndCurveRadius.x; - float rad = depthAndCurveRadius.y; - position.x = mix(rect.x, rect.z, position.x); - position.y = mix(rect.w, rect.y, position.y); - position.z = mix(-depth * 0.5, depth * 0.5, position.z); - if (rad != 0.0) { - float angle = position.x / rad; - position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); - // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); + this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ + color:this.selectionColor ? this.selectionColor : defaultSelectionColor, + transparent: true, + opacity: 0.3, + depthWrite: false + }), + { + uniforms: { + rect: {value: new Vector4( + rect.left , + rect.top , + rect.right , + rect.bottom + )}, + depthAndCurveRadius: {value: new Vector2( + (rect.top - rect.bottom)*THICKNESS, + this.textMesh.curveRadius + )} + }, + vertexDefs: ` + uniform vec4 rect; + uniform vec2 depthAndCurveRadius; + `, + vertexTransform: ` + float depth = depthAndCurveRadius.x; + float rad = depthAndCurveRadius.y; + position.x = mix(rect.x, rect.z, position.x); + position.y = mix(rect.w, rect.y, position.y); + position.z = mix(-depth * 0.5, depth * 0.5, position.z); + if (rad != 0.0) { + float angle = position.x / rad; + position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); + // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); + } + ` } - ` - } - ) - material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] - let selectRect = new Mesh( - this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, - material - // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) - ) - // selectRect.position.x = -1 - // selectRect.position.y = -1 - this.textMesh.add(selectRect) - } + ) + material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] + let selectRect = new Mesh( + this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, + material + // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) + ) + this.selectionRectsMeshs.unshift(selectRect) + this.textMesh.add(selectRect) + }) this.textMesh.updateWorldMatrix(false,true) } @@ -322,19 +322,19 @@ const AccessibleText = /*#__PURE__*/(() => { ){ this.prevCurveRadius = this.textMesh.curveRadius //update geometry - for (let key in this.selectionRects) { - this.textMesh.children[key].geometry = this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry - } + this.selectionRectsMeshs.forEach((rect)=>{ + rect.geometry = this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry + }) } - for (let key in this.selectionRects) { - this.textMesh.children[key].material.uniforms.depthAndCurveRadius.value.y = this.textMesh.curveRadius - if(this.selectionColor != this.textMesh.children[key].material.color){ + this.selectionRectsMeshs.forEach((rect)=>{ + rect.material.uniforms.depthAndCurveRadius.value.y = this.textMesh.curveRadius + if(this.selectionColor != rect.material.color){ //faster to check fo color change or to set needsUpdate true each time ? - //to discuss - this.textMesh.children[key].material.color.set(this.selectionColor) - this.textMesh.children[key].material.needsUpdate = true + //todo + rect.material.color.set(this.selectionColor) + rect.material.needsUpdate = true } - } + }) } /** @@ -355,47 +355,14 @@ const AccessibleText = /*#__PURE__*/(() => { this.textMesh._needsSync = true; this.textMesh.sync() } - - /** - * stop monitoring dom change - */ - stopObservingMutation(){ - this.observer.disconnect(); - } - /** - * @override Custom raycasting to test against the whole text block's max rectangular bounds - * TODO is there any reason to make this more granular, like within individual line or glyph rects? - */ - raycast(raycaster, intersects) { - const {textRenderInfo, curveRadius} = this - if (textRenderInfo) { - const bounds = textRenderInfo.blockBounds - const raycastMesh = curveRadius ? getCurvedRaycastMesh() : getFlatRaycastMesh() - const geom = raycastMesh.geometry - const {position, uv} = geom.attributes - for (let i = 0; i < uv.count; i++) { - let x = bounds[0] + (uv.getX(i) * (bounds[2] - bounds[0])) - const y = bounds[1] + (uv.getY(i) * (bounds[3] - bounds[1])) - let z = 0 - if (curveRadius) { - z = curveRadius - Math.cos(x / curveRadius) * curveRadius - x = Math.sin(x / curveRadius) * curveRadius - } - position.setXYZ(i, x, y, z) - } - geom.boundingSphere = this.geometry.boundingSphere - geom.boundingBox = this.geometry.boundingBox - raycastMesh.matrixWorld = this.matrixWorld - raycastMesh.material.side = this.material.side - tempArray.length = 0 - raycastMesh.raycast(raycaster, tempArray) - for (let i = 0; i < tempArray.length; i++) { - tempArray[i].object = this - intersects.push(tempArray[i]) - } - } - } + destroy(){ + this.observer.disconnect() + this._domElText.remove() + this._domElSelectedText.remove() + this.childrenGeometry.dispose() + this.childrenCurvedGeometry.dispose() + } } From d0623de336709a676010602b962ea1eed11ab2d6 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sun, 7 Mar 2021 19:05:27 +0100 Subject: [PATCH 15/27] dynamic selection manager --- .../src/facade/SelectionManagerFacade.js | 12 ++++++------ packages/troika-three-text/src/AccessibleText.js | 1 + packages/troika-three-text/src/Text.js | 13 +++++++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index eb9be36b..ebc7eeca 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -18,7 +18,6 @@ class SelectionManagerFacade extends ListFacade { constructor (parent, onSelectionChange) { super(parent) const textMesh = parent.threeObject - console.log(textMesh) this.rangeColor = 0x00ccff this.clipRect = noClip @@ -89,8 +88,7 @@ class SelectionManagerFacade extends ListFacade { parent.removeEventListener('dragend', onDragEnd) } - //clear selection if missed click - parent.getSceneFacade().addEventListener('click',(e)=>{ + const onMissClick = e => { let target = e.target do { if(target.$facadeId === textMesh.parent.$facade.$facadeId){ @@ -100,7 +98,10 @@ class SelectionManagerFacade extends ListFacade { } while (target !== null) //clear selection textMesh.a11yManager.clearSelection() - }) + } + + //clear selection if missed click + parent.getSceneFacade().addEventListener('click',onMissClick) parent.addEventListener('dragstart', onDragStart) parent.addEventListener('mousedown', onDragStart) @@ -110,13 +111,12 @@ class SelectionManagerFacade extends ListFacade { textMesh.a11yManager.selectDomText() window.setTimeout(()=>{ textMesh.a11yManager._domElSelectedText.style.pointerEvents = 'none' - console.log('contextmenu') },50) - console.log('contextmenu') }) this._cleanupEvents = () => { onDragEnd() + parent.getSceneFacade().removeEventListener('click',onMissClick) parent.removeEventListener('dragstart', onDragStart) parent.removeEventListener('mousedown', onDragStart) } diff --git a/packages/troika-three-text/src/AccessibleText.js b/packages/troika-three-text/src/AccessibleText.js index c3ca60e7..35155c9c 100644 --- a/packages/troika-three-text/src/AccessibleText.js +++ b/packages/troika-three-text/src/AccessibleText.js @@ -357,6 +357,7 @@ const AccessibleText = /*#__PURE__*/(() => { } destroy(){ + this.clearSelection() this.observer.disconnect() this._domElText.remove() this._domElSelectedText.remove() diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index f537f19b..a7af3bb9 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -68,7 +68,8 @@ const Text = /*#__PURE__*/(() => { 'anchorX', 'anchorY', 'colorRanges', - 'sdfGlyphSize' + 'sdfGlyphSize', + 'selectable' ] const COPYABLE_PROPS = SYNCABLE_PROPS.concat( @@ -97,7 +98,6 @@ const Text = /*#__PURE__*/(() => { this.selectable = false this.domContainer = null - this.a11yManager = new AccessibleText(this) /** * @member {string} text @@ -402,6 +402,15 @@ const Text = /*#__PURE__*/(() => { if (this._needsSync) { this._needsSync = false + if (this.selectable) { + this.a11yManager = new AccessibleText(this) + }else{ + if(this.a11yManager){ + this.a11yManager.destroy() + this.a11yManager = null + } + } + // If there's another sync still in progress, queue if (this._isSyncing) { (this._queuedSyncs || (this._queuedSyncs = [])).push(callback) From c2d6250a2099ea285f2c5fc4a868db926d34de4b Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Mon, 8 Mar 2021 19:43:07 +0100 Subject: [PATCH 16/27] bug fixs, cleaning --- .../troika-3d-text/src/facade/Text3DFacade.js | 2 +- packages/troika-examples/text/TextExample.jsx | 6 ++--- .../src/{AccessibleText.js => A11yManager.js} | 23 +++++++++++-------- packages/troika-three-text/src/Text.js | 11 +++++---- 4 files changed, 25 insertions(+), 17 deletions(-) rename packages/troika-three-text/src/{AccessibleText.js => A11yManager.js} (96%) diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index 789de1f8..4bacf9e5 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -37,8 +37,8 @@ const TEXT_MESH_PROPS = [ 'glyphGeometryDetail', 'sdfGlyphSize', 'debugSDF', - 'supportScreenReader', 'selectable', + 'accessible', 'selectionMaterial' ] diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index 7ec3a15d..43444926 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -123,10 +123,10 @@ class TextExample extends React.Component { useTexture: false, shadows: false, selectable: true, + accessible: true, colorRanges: false, sdfGlyphSize: 6, debugSDF: false, - supportScreenReader: true, camerax:0, cameray:0, cameraz:2, @@ -210,6 +210,7 @@ class TextExample extends React.Component { anchorX: state.anchorX, anchorY: state.anchorY, selectable: state.selectable, + accessible: state.accessible, debugSDF: state.debugSDF, fillOpacity: state.fillOpacity, outlineWidth: state.outlineWidth, @@ -221,7 +222,6 @@ class TextExample extends React.Component { strokeWidth: state.strokeWidth, strokeColor: state.strokeColor, curveRadius: state.curveRadius, - supportScreenReader: state.supportScreenReader, material: material, color: 0xffffff, selectionMaterial: null, @@ -326,8 +326,8 @@ class TextExample extends React.Component { {type: 'boolean', path: "shadows", label: "Shadows"}, {type: 'boolean', path: "colorRanges", label: "colorRanges (WIP)"}, {type: 'boolean', path: "selectable", label: "Selectable (WIP)"}, + {type: 'boolean', path: "accessible", label: "Accessible (WIP)"}, {type: 'select', path: 'selectionColor', label: "Selection Color (WIP)", options: ['white','red','blue']}, - {type: 'boolean', path: "supportScreenReader", label: "Support Screen readers (WIP)"}, {type: 'number', path: "fontSize", label: "fontSize", min: 0.01, max: 0.2, step: 0.01}, {type: 'number', path: "textScale", label: "scale", min: 0.1, max: 10, step: 0.1}, //{type: 'number', path: "textIndent", label: "indent", min: 0.1, max: 1, step: 0.01}, diff --git a/packages/troika-three-text/src/AccessibleText.js b/packages/troika-three-text/src/A11yManager.js similarity index 96% rename from packages/troika-three-text/src/AccessibleText.js rename to packages/troika-three-text/src/A11yManager.js index 35155c9c..633eabd0 100644 --- a/packages/troika-three-text/src/AccessibleText.js +++ b/packages/troika-three-text/src/A11yManager.js @@ -23,16 +23,17 @@ height:10px; transform-origin:0 0; font-size:10px; line-height: 10px; +user-select: all; ` const domSRoutline = ` - line-break: anywhere; - line-height: 0px; - display: flex; - align-items: center; +line-break: anywhere; +line-height: 0px; +display: flex; +align-items: center; ` -const AccessibleText = /*#__PURE__*/(() => { +const A11yManager = /*#__PURE__*/(() => { const defaultSelectionColor = 0xffffff @@ -46,7 +47,7 @@ const AccessibleText = /*#__PURE__*/(() => { * A ThreeJS Mesh that renders a string of text on a plane in 3D space using signed distance * fields (SDF). */ - class AccessibleText { + class A11yManager { constructor(textMesh) { this.textMesh = textMesh @@ -58,7 +59,7 @@ const AccessibleText = /*#__PURE__*/(() => { this.selectedText = null; //todo how to pass a dom container - this.domContainer = this.domContainer ? this.domContainer : document.documentElement + this.domContainer = this.textMesh.domContainer ? this.textMesh.domContainer : document.documentElement this.domContainer.appendChild(this._domElSelectedText) this.domContainer.appendChild(this._domElText) @@ -122,6 +123,9 @@ const AccessibleText = /*#__PURE__*/(() => { this.prevHTML = this.currentHTML this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); } + + if(!this.textMesh.selectable && this.selectionRects.length != 0) + this.clearSelection() } /** @@ -258,6 +262,7 @@ const AccessibleText = /*#__PURE__*/(() => { //todo manage rect update in a cleaner way. Currently we recreate everything everytime //clean dispose of material no need to do it for geometry because we reuse the same this.selectionRectsMeshs.forEach((rect)=>{ + if(rect.parent) rect.parent.remove(rect) rect.material.dispose() }) @@ -367,9 +372,9 @@ const AccessibleText = /*#__PURE__*/(() => { } - return AccessibleText + return A11yManager })() export { - AccessibleText + A11yManager } diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index a7af3bb9..f2450d04 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -11,7 +11,7 @@ import { import { GlyphsGeometry } from './GlyphsGeometry.js' import { createTextDerivedMaterial } from './TextDerivedMaterial.js' import { getTextRenderInfo } from './TextBuilder.js' -import { AccessibleText } from './AccessibleText' +import { A11yManager } from './A11yManager' const Text = /*#__PURE__*/(() => { @@ -69,7 +69,8 @@ const Text = /*#__PURE__*/(() => { 'anchorY', 'colorRanges', 'sdfGlyphSize', - 'selectable' + 'selectable', + 'accessible' ] const COPYABLE_PROPS = SYNCABLE_PROPS.concat( @@ -97,6 +98,7 @@ const Text = /*#__PURE__*/(() => { // === Text layout properties: === // this.selectable = false + this.accessible = false this.domContainer = null /** @@ -402,8 +404,9 @@ const Text = /*#__PURE__*/(() => { if (this._needsSync) { this._needsSync = false - if (this.selectable) { - this.a11yManager = new AccessibleText(this) + if (this.selectable || this.accessible) { + if(!this.a11yManager) + this.a11yManager = new A11yManager(this) }else{ if(this.a11yManager){ this.a11yManager.destroy() From f15d0ce6a742a0fb4d91ff8991c258a2eb8d7391 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Mon, 8 Mar 2021 20:57:39 +0100 Subject: [PATCH 17/27] fix selection color + comments --- packages/troika-examples/text/TextExample.jsx | 2 +- packages/troika-three-text/src/A11yManager.js | 30 ++-------- packages/troika-three-text/src/Text.js | 60 ++++++++++++++----- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index 43444926..33ac8f3c 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -225,7 +225,7 @@ class TextExample extends React.Component { material: material, color: 0xffffff, selectionMaterial: null, - selectionColor: 0xfffff, + selectionColor: state.selectionColor, scaleX: state.textScale || 1, scaleY: state.textScale || 1, scaleZ: state.textScale || 1, diff --git a/packages/troika-three-text/src/A11yManager.js b/packages/troika-three-text/src/A11yManager.js index 633eabd0..5bef2213 100644 --- a/packages/troika-three-text/src/A11yManager.js +++ b/packages/troika-three-text/src/A11yManager.js @@ -82,27 +82,6 @@ const A11yManager = /*#__PURE__*/(() => { this.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) /* create it only once */ this.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1,32).translate(0.5, 0.5, 0.5) - - /** - * @member {THREE.Material} selectionMaterial - * Defines a _base_ material to be used when rendering the text. This material will be - * automatically replaced with a material derived from it, that adds shader code to - * decrease the alpha for each fragment (pixel) outside the text glyphs, with antialiasing. - * By default it will derive from a simple white MeshBasicMaterial, but you can use any - * of the other mesh materials to gain other features like lighting, texture maps, etc. - * - * Also see the `selectionColor` shortcut property. - */ - this.selectionMaterial = null - - /** - * @member {string|number|THREE.Color} selectionColor - * This is a shortcut for setting the `color` of the text's material. You can use this - * if you don't want to specify a whole custom `material`. Also, if you do use a custom - * `material`, this color will only be used for this particuar Text instance, even if - * that same material instance is shared across multiple Text objects. - */ - this.selectionColor = defaultSelectionColor } @@ -258,7 +237,6 @@ const A11yManager = /*#__PURE__*/(() => { highlightText() { let THICKNESS = 0.25; - //todo manage rect update in a cleaner way. Currently we recreate everything everytime //clean dispose of material no need to do it for geometry because we reuse the same this.selectionRectsMeshs.forEach((rect)=>{ @@ -270,8 +248,8 @@ const A11yManager = /*#__PURE__*/(() => { this.selectionRects.forEach((rect)=>{ let material = createDerivedMaterial( - this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ - color:this.selectionColor ? this.selectionColor : defaultSelectionColor, + this.textMesh.selectionMaterial ? this.textMesh.selectionMaterial : new MeshBasicMaterial({ + color:this.textMesh.selectionColor ? this.textMesh.selectionColor : defaultSelectionColor, transparent: true, opacity: 0.3, depthWrite: false @@ -333,10 +311,10 @@ const A11yManager = /*#__PURE__*/(() => { } this.selectionRectsMeshs.forEach((rect)=>{ rect.material.uniforms.depthAndCurveRadius.value.y = this.textMesh.curveRadius - if(this.selectionColor != rect.material.color){ + if(this.textMesh.selectionColor != rect.material.color){ //faster to check fo color change or to set needsUpdate true each time ? //todo - rect.material.color.set(this.selectionColor) + rect.material.color.set(this.textMesh.selectionColor) rect.material.needsUpdate = true } }) diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index f2450d04..729c578a 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -97,10 +97,6 @@ const Text = /*#__PURE__*/(() => { // === Text layout properties: === // - this.selectable = false - this.accessible = false - this.domContainer = null - /** * @member {string} text * The string of text to be rendered. @@ -228,18 +224,6 @@ const Text = /*#__PURE__*/(() => { */ this.material = null - /** - * @member {THREE.Material} selectionMaterial - * Defines a _base_ material to be used when rendering the text. This material will be - * automatically replaced with a material derived from it, that adds shader code to - * decrease the alpha for each fragment (pixel) outside the text glyphs, with antialiasing. - * By default it will derive from a simple white MeshBasicMaterial, but you can use any - * of the other mesh materials to gain other features like lighting, texture maps, etc. - * - * Also see the `selectionColor` shortcut property. - */ - this.selectionMaterial = null - /** * @member {string|number|THREE.Color} color * This is a shortcut for setting the `color` of the text's material. You can use this @@ -391,6 +375,50 @@ const Text = /*#__PURE__*/(() => { */ this.sdfGlyphSize = null + // === dom related properties === // + + /** + * @member {boolean} selectable + * Defines whether the displayed text can be selected by the user in order to be copied to its + * clipboard for instance. This will automatically enable the accessible mode. + */ + this.selectable = false + + /** + * @member {boolean} accessible + * Defines whether the displayed text can be read by screen readers or not. + * Enabling this also allow the user to visually translate the text using browser translating tools. + * This setting use overlaying HTML that is kept above the rendered text. + */ + this.accessible = false + + /** + * @member {THREE.Material} selectionMaterial + * Defines a _base_ material to be used when rendering the text selection. This material will be + * automatically replaced with a material derived from it, that adds shader code to manage + * curved text. + * By default it will derive from a simple white MeshBasicMaterial with alpha of 0.3, but you can use any + * of the other mesh materials to gain other features like lighting, texture maps, etc. + * + * Also see the `selectionColor` shortcut property. + */ + this.selectionMaterial = null + + /** + * @member {string|number|THREE.Color} selectionColor + * This is a shortcut for setting the `color` of the text selection's material. You can use this + * if you don't want to specify a whole custom `material`. Also, if you do use a custom + * `material`, this color will only be used for this particuar Text instance, even if + * that same material instance is shared across multiple Text objects. + */ + this.selectionColor = null + + /** + * @member {element} domContainer + * When set, the overlaying HTML of the selection or accessible feature will be a child of this container. + */ + this.domContainer = null + this.debugSDF = false } From f8122997920b02c7561d871ae5326c9e30430807 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Wed, 24 Mar 2021 00:14:40 +0100 Subject: [PATCH 18/27] separate selectable and a11y in two make functions --- .../src/facade/SelectionManagerFacade.js | 32 +- .../troika-3d-text/src/facade/Text3DFacade.js | 11 +- packages/troika-examples/text/TextExample.jsx | 136 ++++----- packages/troika-three-text/src/Text.js | 49 +-- packages/troika-three-text/src/index.js | 2 + .../troika-three-text/src/makeDOMAcessible.js | 162 ++++++++++ .../troika-three-text/src/makeSelectable.js | 285 ++++++++++++++++++ 7 files changed, 558 insertions(+), 119 deletions(-) create mode 100644 packages/troika-three-text/src/makeDOMAcessible.js create mode 100644 packages/troika-three-text/src/makeSelectable.js diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index ebc7eeca..f4a497fe 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -15,7 +15,7 @@ const noClip = Object.freeze([-Infinity, -Infinity, Infinity, Infinity]) * Manager facade for selection rects and user selection behavior */ class SelectionManagerFacade extends ListFacade { - constructor (parent, onSelectionChange) { + constructor(parent, onSelectionChange) { super(parent) const textMesh = parent.threeObject @@ -41,13 +41,13 @@ class SelectionManagerFacade extends ListFacade { } const onDragStart = e => { - if(e.which===3){//contextmenu + if (e.which === 3) {//contextmenu return false } const textRenderInfo = textMesh.textRenderInfo if (textRenderInfo) { const textPos = textMesh.worldPositionToTextCoords(e.intersection.point, tempVec2) - const caret = textMesh.a11yManager.startCaret(textRenderInfo,textPos.x, textPos.y) + const caret = textMesh.startCaret(textRenderInfo, textPos.x, textPos.y) if (caret) { onSelectionChange(caret.charIndex, caret.charIndex) parent.addEventListener('drag', onDrag) @@ -58,7 +58,7 @@ class SelectionManagerFacade extends ListFacade { } const onDrag = e => { - if(e.which===3){//contextmenu + if (e.which === 3) {//contextmenu return false } const textRenderInfo = textMesh.textRenderInfo @@ -74,7 +74,7 @@ class SelectionManagerFacade extends ListFacade { // textPos = ray.intersectPlane(tempPlane.setComponents(0, 0, 1, 0), tempVec3) } if (textPos) { - const caret = textMesh.a11yManager.moveCaret(textRenderInfo,textPos.x, textPos.y) + const caret = textMesh.moveCaret(textRenderInfo, textPos.x, textPos.y) if (caret) { onSelectionChange(this.selectionStart, caret.charIndex) } @@ -91,32 +91,32 @@ class SelectionManagerFacade extends ListFacade { const onMissClick = e => { let target = e.target do { - if(target.$facadeId === textMesh.parent.$facade.$facadeId){ + if (target.$facadeId === textMesh.parent.$facade.$facadeId) { return } target = target.parent } while (target !== null) //clear selection - textMesh.a11yManager.clearSelection() + textMesh.clearSelection() } //clear selection if missed click - parent.getSceneFacade().addEventListener('click',onMissClick) + parent.getSceneFacade().addEventListener('click', onMissClick) parent.addEventListener('dragstart', onDragStart) parent.addEventListener('mousedown', onDragStart) var canvas = parent.getSceneFacade().parent._threeRenderer.domElement; - canvas.addEventListener('contextmenu',(e)=>{ - textMesh.a11yManager._domElSelectedText.style.pointerEvents = 'auto' - textMesh.a11yManager.selectDomText() - window.setTimeout(()=>{ - textMesh.a11yManager._domElSelectedText.style.pointerEvents = 'none' - },50) + canvas.addEventListener('contextmenu', (e) => { + textMesh._domElSelectedText.style.pointerEvents = 'auto' + textMesh.selectDomText() + window.setTimeout(() => { + textMesh._domElSelectedText.style.pointerEvents = 'none' + }, 50) }) this._cleanupEvents = () => { onDragEnd() - parent.getSceneFacade().removeEventListener('click',onMissClick) + parent.getSceneFacade().removeEventListener('click', onMissClick) parent.removeEventListener('dragstart', onDragStart) parent.removeEventListener('mousedown', onDragStart) } @@ -134,7 +134,7 @@ class SelectionManagerFacade extends ListFacade { return this._clipRect } - destructor () { + destructor() { this._cleanupEvents() super.destructor() } diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index 4bacf9e5..eee68dac 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -1,5 +1,5 @@ import { Object3DFacade } from 'troika-3d' -import { Text } from 'troika-three-text' +import { Text, makeDOMAcessible, makeSelectable } from 'troika-three-text' import SelectionManagerFacade from './SelectionManagerFacade.js' // Properties that will simply be forwarded to the TextMesh: @@ -96,6 +96,13 @@ class Text3DFacade extends Object3DFacade { super.afterUpdate() + if (this.accessible && !this.threeObject.isDOMAccessible) { + makeDOMAcessible(this.threeObject) + } + if (this.selectable && !this.threeObject.isSelectable) { + makeSelectable(this.threeObject) + } + if (this.text !== this._prevText) { // TODO mirror to DOM... this._domEl.textContent = this.text // Clear selection when text changes @@ -107,7 +114,7 @@ class Text3DFacade extends Object3DFacade { } _updateSelection() { - const {selectable, selectionStart, selectionEnd} = this + const { selectable, selectionStart, selectionEnd } = this let selFacade = this._selectionFacade if (selectable !== this._selectable) { this._selectable = selectable diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index 33ac8f3c..e21b2e92 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -1,7 +1,7 @@ import React from 'react' import T from 'prop-types' import { Canvas3D, createDerivedMaterial, Object3DFacade } from 'troika-3d' -import {Text3DFacade, dumpSDFTextures} from 'troika-3d-text' +import { Text3DFacade, dumpSDFTextures } from 'troika-3d-text' import { MeshBasicMaterial, MeshStandardMaterial, @@ -11,7 +11,7 @@ import { Color, DoubleSide } from 'three' -import DatGui, {DatBoolean, DatSelect, DatNumber} from 'react-dat-gui' +import DatGui, { DatBoolean, DatSelect, DatNumber } from 'react-dat-gui' import { DatGuiFacade } from 'troika-3d-ui' import { ExampleConfigurator } from '../_shared/ExampleConfigurator.js' @@ -127,10 +127,10 @@ class TextExample extends React.Component { colorRanges: false, sdfGlyphSize: 6, debugSDF: false, - camerax:0, - cameray:0, - cameraz:2, - lookAt:{x: 0, y: 0, z: 0} + camerax: 0, + cameray: 0, + cameraz: 2, + lookAt: { x: 0, y: 0, z: 0 } } this._onConfigUpdate = (newState) => { @@ -144,7 +144,7 @@ class TextExample extends React.Component { render() { let state = this.state - let {width, height} = this.props + let { width, height } = this.props let material = state.material if (state.useTexture) material += '+Texture' @@ -154,20 +154,20 @@ class TextExample extends React.Component {
{/* diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 729c578a..1534b32e 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -11,8 +11,6 @@ import { import { GlyphsGeometry } from './GlyphsGeometry.js' import { createTextDerivedMaterial } from './TextDerivedMaterial.js' import { getTextRenderInfo } from './TextBuilder.js' -import { A11yManager } from './A11yManager' - const Text = /*#__PURE__*/(() => { @@ -51,8 +49,10 @@ const Text = /*#__PURE__*/(() => { return mesh } - const syncStartEvent = {type: 'syncstart'} - const syncCompleteEvent = {type: 'synccomplete'} + const syncStartEvent = { type: 'syncstart' } + const syncCompleteEvent = { type: 'synccomplete' } + const beforeRenderEvent = { type: 'beforerender' } + const afterRenderEvent = { type: 'afterrender' } const SYNCABLE_PROPS = [ 'font', @@ -432,16 +432,6 @@ const Text = /*#__PURE__*/(() => { if (this._needsSync) { this._needsSync = false - if (this.selectable || this.accessible) { - if(!this.a11yManager) - this.a11yManager = new A11yManager(this) - }else{ - if(this.a11yManager){ - this.a11yManager.destroy() - this.a11yManager = null - } - } - // If there's another sync still in progress, queue if (this._isSyncing) { (this._queuedSyncs || (this._queuedSyncs = [])).push(callback) @@ -449,10 +439,6 @@ const Text = /*#__PURE__*/(() => { this._isSyncing = true this.dispatchEvent(syncStartEvent) - if(this.a11yManager){ - this.a11yManager.sync() - } - this.currentText = this.currentText ? this.currentText : this.text getTextRenderInfo({ @@ -505,13 +491,6 @@ const Text = /*#__PURE__*/(() => { } } - onAfterRender(renderer, scene, camera) { - if(this.a11yManager){ - this.a11yManager.updateDomPosition(renderer, camera) - this.a11yManager.updateSelectedDomPosition(renderer, camera) - } - } - /** * Initiate a sync if needed - note it won't complete until next frame at the * earliest so if possible it's a good idea to call sync() manually as soon as @@ -521,9 +500,7 @@ const Text = /*#__PURE__*/(() => { onBeforeRender(renderer, scene, camera, geometry, material, group) { this.sync() - if(this.a11yManager){ - this.a11yManager.updateHighlightTextUniforms() - } + this.dispatchEvent(beforeRenderEvent) // This may not always be a text material, e.g. if there's a scene.overrideMaterial present if (material.isTroikaTextMaterial) { @@ -531,6 +508,12 @@ const Text = /*#__PURE__*/(() => { } } + onAfterRender(renderer, scene, camera) { + this.renderer = renderer + this.camera = camera + this.dispatchEvent(afterRenderEvent) + } + /** * Shortcut to dispose the geometry specific to this instance. * Note: we don't also dispose the derived material here because if anything else is @@ -574,7 +557,7 @@ const Text = /*#__PURE__*/(() => { let outlineMaterial = derivedMaterial._outlineMtl if (!outlineMaterial) { outlineMaterial = derivedMaterial._outlineMtl = Object.create(derivedMaterial, { - id: {value: derivedMaterial.id + 0.1} + id: { value: derivedMaterial.id + 0.1 } }) outlineMaterial.isTextOutlineMaterial = true outlineMaterial.depthWrite = false @@ -628,7 +611,7 @@ const Text = /*#__PURE__*/(() => { const uniforms = material.uniforms const textInfo = this.textRenderInfo if (textInfo) { - const {sdfTexture, blockBounds} = textInfo + const { sdfTexture, blockBounds } = textInfo uniforms.uTroikaSDFTexture.value = sdfTexture uniforms.uTroikaSDFTextureSize.value.set(sdfTexture.image.width, sdfTexture.image.height) uniforms.uTroikaSDFGlyphSize.value = textInfo.sdfGlyphSize @@ -646,7 +629,7 @@ const Text = /*#__PURE__*/(() => { let offsetY = 0 if (isOutline) { - let {outlineWidth, outlineOffsetX, outlineOffsetY, outlineBlur, outlineOpacity} = this + let { outlineWidth, outlineOffsetX, outlineOffsetY, outlineBlur, outlineOpacity } = this distanceOffset = this._parsePercent(outlineWidth) || 0 blurRadius = Math.max(0, this._parsePercent(outlineBlur) || 0) fillOpacity = outlineOpacity @@ -756,12 +739,12 @@ const Text = /*#__PURE__*/(() => { * TODO is there any reason to make this more granular, like within individual line or glyph rects? */ raycast(raycaster, intersects) { - const {textRenderInfo, curveRadius} = this + const { textRenderInfo, curveRadius } = this if (textRenderInfo) { const bounds = textRenderInfo.blockBounds const raycastMesh = curveRadius ? getCurvedRaycastMesh() : getFlatRaycastMesh() const geom = raycastMesh.geometry - const {position, uv} = geom.attributes + const { position, uv } = geom.attributes for (let i = 0; i < uv.count; i++) { let x = bounds[0] + (uv.getX(i) * (bounds[2] - bounds[0])) const y = bounds[1] + (uv.getY(i) * (bounds[3] - bounds[1])) diff --git a/packages/troika-three-text/src/index.js b/packages/troika-three-text/src/index.js index 420ec596..2554aebb 100644 --- a/packages/troika-three-text/src/index.js +++ b/packages/troika-three-text/src/index.js @@ -5,3 +5,5 @@ export { Text } from './Text.js' export { GlyphsGeometry } from './GlyphsGeometry.js' export { createTextDerivedMaterial } from './TextDerivedMaterial.js' export { getCaretAtPoint, getSelectionRects } from './selectionUtils.js' +export { makeDOMAcessible } from './makeDOMAcessible.js' +export { makeSelectable } from './makeSelectable.js' diff --git a/packages/troika-three-text/src/makeDOMAcessible.js b/packages/troika-three-text/src/makeDOMAcessible.js new file mode 100644 index 00000000..ee7bb960 --- /dev/null +++ b/packages/troika-three-text/src/makeDOMAcessible.js @@ -0,0 +1,162 @@ +import { + Matrix4, + Vector3, +} from 'three' + +const domOverlayBaseStyles = ` +position:fixed; +top:0; +left:0; +opacity:0; +overflow:hidden; +margin:0; +pointer-events:none; +width:10px; +height:10px; +transform-origin:0 0; +font-size:10px; +line-height: 10px; +user-select: all; +` + +const domSRoutline = ` +line-break: anywhere; +line-height: 0px; +display: flex; +align-items: center; +` + +const makeDOMAcessible = (textInstance, options = {}) => { + + const tempMat4a = new Matrix4() + const tempMat4b = new Matrix4() + const tempVec3 = new Vector3() + + const _options = Object.assign({ + domContainer: document.documentElement, + tagName: 'p', + observeMutation: true + }, options); + + textInstance._domElText = document.createElement(_options.tagName) + + textInstance.domContainer = _options.domContainer + textInstance.domContainer.appendChild(textInstance._domElText) + textInstance._domElText.style = domOverlayBaseStyles + domSRoutline + textInstance.isDOMAccessible = true + + /** + * When a change occurs on the overlaying HTML, it reflect it in the renderer context + */ + textInstance.mutationCallback = function (mutationsList, observer) { + this.currentHTML = this._domElText.innerHTML + this.currentText = this.currentHTML.replace(/<(?!br\s*\/?)[^>]+>/g, '').replace(//gi, "\n"); + this._needsSync = true; + this.sync() + } + + if (_options.observeMutation) { + /** + * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context + */ + textInstance.startObservingMutation = function () { + textInstance.observer = new MutationObserver(textInstance.mutationCallback.bind(textInstance)); + textInstance.observer.observe(textInstance._domElText, { attributes: false, childList: true, subtree: false }); + } + textInstance.startObservingMutation() + } + + textInstance.prevText = '' + textInstance.currentText = '' + textInstance.prevHTML = '' + textInstance.currentHTML = '' + textInstance.prevCurveRadius = 0 + textInstance.pauseDomSync = false + + textInstance.syncDOM = function () { + if (this.prevText !== this.text) { + this.currentText = this.text + this.prevHTML = this.currentHTML + this.currentHTML = this.text.replace(/(?:\r\n|\r|\n)/g, '
') + this.prevText = this.text + } + + this.currentText = this.currentText ? this.currentText : this.text + + //update dom with latest text + if (this.prevHTML !== this.currentHTML) { + this.observer.disconnect() + this._domElText.innerHTML = this.currentHTML; + this.prevHTML = this.currentHTML + this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); + } + } + + textInstance.addEventListener('syncstart', textInstance.syncDOM) + + textInstance.syncDOM() + + /** + * update the position of the overlaying HTML that contain all the text that need to be accessible to screen readers + */ + textInstance.updateDomPosition = function (renderer, camera) { + if (!this.pauseDomSync) { + const { min, max } = this.geometry.boundingBox + this._domElText.style.transform = this._textRectToCssMatrix(min.x, min.y, max.x, max.y, max.z, renderer, camera) + } + } + + textInstance.addEventListener('afterrender', function () { + const renderer = this.renderer + const camera = this.camera + this.updateDomPosition(renderer, camera) + }) + + /** + * Given a rect in local text coordinates, build a CSS matrix3d that will transform + * a 10x10 DOM element to line up exactly with that rect on the screen. + * @private + */ + textInstance._textRectToCssMatrix = function (minX, minY, maxX, maxY, z, renderer, camera) { + const canvasRect = renderer.domElement.getBoundingClientRect() + + // element dimensions to geometry dimensions (flipping the y) + tempMat4a.makeScale((maxX - minX) / 10, (minY - maxY) / 10, 1) + .setPosition(tempVec3.set(minX, maxY, z)) + + // geometry to world + tempMat4a.premultiply(this.matrixWorld) + + // world to camera + tempMat4a.premultiply(camera.matrixWorldInverse) + + // camera to projection + tempMat4a.premultiply(camera.projectionMatrix) + + // projection coords (-1 to 1) to screen pixels + tempMat4a.premultiply( + tempMat4b.makeScale(canvasRect.width / 2, -canvasRect.height / 2, 1) + .setPosition(canvasRect.left + canvasRect.width / 2, canvasRect.top + canvasRect.height / 2, 0) + ) + + return `matrix3d(${tempMat4a.elements.join(',')})` + } + + textInstance.pause = function () { + this.pauseDomSync = true + } + + textInstance.resume = function () { + this.pauseDomSync = false + } + + textInstance.destroy = function () { + this.observer.disconnect() + this._domElText.remove() + } + +} + +export { + makeDOMAcessible +} diff --git a/packages/troika-three-text/src/makeSelectable.js b/packages/troika-three-text/src/makeSelectable.js new file mode 100644 index 00000000..a228fd53 --- /dev/null +++ b/packages/troika-three-text/src/makeSelectable.js @@ -0,0 +1,285 @@ +import { + Matrix4, + Mesh, + MeshBasicMaterial, + Vector4, + Vector3, + Vector2, + BoxBufferGeometry +} from 'three' +import { createDerivedMaterial } from 'troika-three-utils' +import { getSelectionRects, getCaretAtPoint } from './selectionUtils' + +const domOverlayBaseStyles = ` +position:fixed; +top:0; +left:0; +opacity:0; +overflow:hidden; +margin:0; +pointer-events:none; +width:10px; +height:10px; +transform-origin:0 0; +font-size:10px; +line-height: 10px; +user-select: all; +` + +const makeSelectable = (textInstance, eventEmitter) => { + + const defaultSelectionColor = 0xffffff + + const tempMat4a = new Matrix4() + const tempMat4b = new Matrix4() + const tempVec3 = new Vector3() + + textInstance._domElSelectedText = document.createElement('p') + textInstance.selectionStartIndex = 0; + textInstance.selectionEndIndex = 0; + textInstance.selectedText = null; + + textInstance.domContainer = document.documentElement + textInstance.domContainer.appendChild(textInstance._domElSelectedText) + + textInstance._domElSelectedText.setAttribute('aria-hidden', 'true') + textInstance._domElSelectedText.style = domOverlayBaseStyles + + textInstance.selectionRects = [] + textInstance.selectionRectsMeshs = [] + + textInstance.prevCurveRadius = 0 + textInstance.isSelectable = true + + textInstance.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) + textInstance.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1, 32).translate(0.5, 0.5, 0.5) + + textInstance.addEventListener('syncstart', function () { + if (!this.selectable && this.selectionRects.length != 0) + this.clearSelection() + }) + + /** + * Given a local x/y coordinate in the text block plane, set the start position of the caret + * used in text selection + * @param {number} x + * @param {number} y + * @return {TextCaret | null} + */ + textInstance.startCaret = function (textRenderInfo, x, y) { + let caret = getCaretAtPoint(textRenderInfo, x, y) + this.selectionStartIndex = caret.charIndex + this.selectionEndIndex = caret.charIndex + this.updateSelection(textRenderInfo) + return caret + } + + textInstance.clearSelection = function () { + this.selectionStartIndex = 0 + this.selectionEndIndex = 0 + this.selectionRects = [] + this._domElSelectedText.textContent = '' + this.highlightText() + } + + /** + * Given a local x/y coordinate in the text block plane, set the end position of the caret + * used in text selection + * @param {number} x + * @param {number} y + * @return {TextCaret | null} + */ + textInstance.moveCaret = function (textRenderInfo, x, y) { + let caret = getCaretAtPoint(textRenderInfo, x, y) + this.selectionEndIndex = caret.charIndex + this.updateSelection(textRenderInfo) + return caret + } + + /** + * update the selection visually and everything related to copy /paste + */ + textInstance.updateSelection = function (textRenderInfo) { + this.selectedText = this.text.substring(this.selectionStartIndex, this.selectionEndIndex) + this.selectionRects = getSelectionRects(textRenderInfo, this.selectionStartIndex, this.selectionEndIndex) + this._domElSelectedText.textContent = this.selectedText + this.highlightText() + this.selectDomText() + } + + /** + * Select the text contened in _domElSelectedText in order for it to reflect what's currently selected in the Text + */ + textInstance.selectDomText = function () { + const sel = document.getSelection() + sel.removeAllRanges() + const range = document.createRange() + range.selectNodeContents(this._domElSelectedText); //sets Range + sel.removeAllRanges(); //remove all ranges from selection + sel.addRange(range); + } + + /** + * update the position of the overlaying HTML that contain + * the selected text in order for it to be acessible through context menu copy + */ + textInstance.updateSelectedDomPosition = function (renderer, camera) { + console.log('updateSelectedDomPosition') + const rects = this.selectionRects + const el = this._domElSelectedText + if (rects && rects.length) { + // Find local space rect containing all selection rects + // TODO can we wrap this even tighter to multiline selections where top/bottom lines are partially selected? + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity + for (let i = 0; i < rects.length; i++) { + minX = Math.min(minX, rects[i].left) + minY = Math.min(minY, rects[i].bottom) + maxX = Math.max(maxX, rects[i].right) + maxY = Math.max(maxY, rects[i].top) + } + + const z = this.geometry.boundingBox.max.z + el.style.transform = this._textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera) + el.style.display = 'block' + } else { + el.style.display = 'none' + } + } + + /** + * Given a rect in local text coordinates, build a CSS matrix3d that will transform + * a 10x10 DOM element to line up exactly with that rect on the screen. + * @private + */ + textInstance._textRectToCssMatrix = function (minX, minY, maxX, maxY, z, renderer, camera) { + const canvasRect = renderer.domElement.getBoundingClientRect() + + // element dimensions to geometry dimensions (flipping the y) + tempMat4a.makeScale((maxX - minX) / 10, (minY - maxY) / 10, 1) + .setPosition(tempVec3.set(minX, maxY, z)) + + // geometry to world + tempMat4a.premultiply(this.matrixWorld) + + // world to camera + tempMat4a.premultiply(camera.matrixWorldInverse) + + // camera to projection + tempMat4a.premultiply(camera.projectionMatrix) + + // projection coords (-1 to 1) to screen pixels + tempMat4a.premultiply( + tempMat4b.makeScale(canvasRect.width / 2, -canvasRect.height / 2, 1) + .setPosition(canvasRect.left + canvasRect.width / 2, canvasRect.top + canvasRect.height / 2, 0) + ) + + return `matrix3d(${tempMat4a.elements.join(',')})` + } + + /** + * visually update the rendering of the text selection in the renderer context + */ + textInstance.highlightText = function () { + let THICKNESS = 0.25; + //todo manage rect update in a cleaner way. Currently we recreate everything everytime + //clean dispose of material no need to do it for geometry because we reuse the same + this.selectionRectsMeshs.forEach((rect) => { + if (rect.parent) + rect.parent.remove(rect) + rect.material.dispose() + }) + this.selectionRectsMeshs = [] + + this.selectionRects.forEach((rect) => { + let material = createDerivedMaterial( + this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ + color: this.selectionColor ? this.selectionColor : defaultSelectionColor, + transparent: true, + opacity: 0.3, + depthWrite: false + }), + { + uniforms: { + rect: { + value: new Vector4( + rect.left, + rect.top, + rect.right, + rect.bottom + ) + }, + depthAndCurveRadius: { + value: new Vector2( + (rect.top - rect.bottom) * THICKNESS, + this.curveRadius + ) + } + }, + vertexDefs: ` + uniform vec4 rect; + uniform vec2 depthAndCurveRadius; + `, + vertexTransform: ` + float depth = depthAndCurveRadius.x; + float rad = depthAndCurveRadius.y; + position.x = mix(rect.x, rect.z, position.x); + position.y = mix(rect.w, rect.y, position.y); + position.z = mix(-depth * 0.5, depth * 0.5, position.z); + if (rad != 0.0) { + float angle = position.x / rad; + position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); + // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); + } + ` + } + ) + material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] + let selectRect = new Mesh( + this.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, + material + // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) + ) + this.selectionRectsMeshs.unshift(selectRect) + this.add(selectRect) + }) + this.updateWorldMatrix(false, true) + } + + textInstance.updateHighlightTextUniforms = function () { + if ( + this.prevCurveRadius === 0 && this.curveRadius !== 0 + || + this.prevCurveRadius !== 0 && this.curveRadius === 0 + ) { + this.prevCurveRadius = this.curveRadius + //update geometry + this.selectionRectsMeshs.forEach((rect) => { + rect.geometry = this.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry + }) + } + this.selectionRectsMeshs.forEach((rect) => { + rect.material.uniforms.depthAndCurveRadius.value.y = this.curveRadius + if (this.selectionColor != rect.material.color) { + //faster to check fo color change or to set needsUpdate true each time ? + //todo + rect.material.color.set(this.selectionColor) + rect.material.needsUpdate = true + } + }) + } + + textInstance.addEventListener('beforerender', function () { + this.updateHighlightTextUniforms() + }) + + textInstance.addEventListener('afterrender', function () { + const renderer = this.renderer + const camera = this.camera + this.updateSelectedDomPosition(renderer, camera) + }) + +} + +export { + makeSelectable +} From 671d97192fd7e972d4fc0b7a9453c3d7fd797b29 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Wed, 24 Mar 2021 15:06:52 +0100 Subject: [PATCH 19/27] split textHighlighter --- packages/troika-three-text/src/A11yManager.js | 358 ------------------ .../troika-three-text/src/TextHighlighter.js | 135 +++++++ packages/troika-three-text/src/index.js | 1 + .../troika-three-text/src/makeSelectable.js | 114 +----- 4 files changed, 142 insertions(+), 466 deletions(-) delete mode 100644 packages/troika-three-text/src/A11yManager.js create mode 100644 packages/troika-three-text/src/TextHighlighter.js diff --git a/packages/troika-three-text/src/A11yManager.js b/packages/troika-three-text/src/A11yManager.js deleted file mode 100644 index 5bef2213..00000000 --- a/packages/troika-three-text/src/A11yManager.js +++ /dev/null @@ -1,358 +0,0 @@ -import { - Matrix4, - Mesh, - MeshBasicMaterial, - Vector4, - Vector3, - Vector2, - BoxBufferGeometry -} from 'three' -import { createDerivedMaterial } from 'troika-three-utils' -import { getSelectionRects, getCaretAtPoint } from './selectionUtils' - -const domOverlayBaseStyles = ` -position:fixed; -top:0; -left:0; -opacity:0; -overflow:hidden; -margin:0; -pointer-events:none; -width:10px; -height:10px; -transform-origin:0 0; -font-size:10px; -line-height: 10px; -user-select: all; -` - -const domSRoutline = ` -line-break: anywhere; -line-height: 0px; -display: flex; -align-items: center; -` - -const A11yManager = /*#__PURE__*/(() => { - - const defaultSelectionColor = 0xffffff - - const tempMat4a = new Matrix4() - const tempMat4b = new Matrix4() - const tempVec3 = new Vector3() - - /** - * @class Text - * - * A ThreeJS Mesh that renders a string of text on a plane in 3D space using signed distance - * fields (SDF). - */ - class A11yManager { - constructor(textMesh) { - - this.textMesh = textMesh - - this._domElSelectedText = document.createElement('p') - this._domElText = document.createElement(this.tagName ? this.tagName : 'p') - this.selectionStartIndex = 0; - this.selectionEndIndex = 0; - this.selectedText = null; - - //todo how to pass a dom container - this.domContainer = this.textMesh.domContainer ? this.textMesh.domContainer : document.documentElement - this.domContainer.appendChild(this._domElSelectedText) - this.domContainer.appendChild(this._domElText) - - this._domElSelectedText.setAttribute('aria-hidden','true') - this._domElSelectedText.style = domOverlayBaseStyles - this._domElText.style = domOverlayBaseStyles + domSRoutline - - this.startObservingMutation() - - this.selectionRects = [] - this.selectionRectsMeshs = [] - - this.prevText = '' - this.currentText = '' - this.prevHTML = '' - this.currentHTML = '' - this.prevCurveRadius = 0 - - /* create it only once */ - this.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) - /* create it only once */ - this.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1,32).translate(0.5, 0.5, 0.5) - } - - - sync() { - if(this.prevText !== this.textMesh.text){ - this.textMesh.currentText = this.textMesh.text - this.prevHTML = this.currentHTML - this.currentHTML = this.textMesh.text.replace(/(?:\r\n|\r|\n)/g, '
') - this.prevText = this.textMesh.text - } - - this.textMesh.currentText = this.textMesh.currentText ? this.textMesh.currentText : this.textMesh.text - - //update dom with latest text - if(this.prevHTML !== this.currentHTML){ - this.observer.disconnect() - this._domElText.innerHTML = this.currentHTML; - this.prevHTML = this.currentHTML - this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); - } - - if(!this.textMesh.selectable && this.selectionRects.length != 0) - this.clearSelection() - } - - /** - * Given a local x/y coordinate in the text block plane, set the start position of the caret - * used in text selection - * @param {number} x - * @param {number} y - * @return {TextCaret | null} - */ - startCaret(textRenderInfo,x,y){ - let caret = getCaretAtPoint(textRenderInfo, x, y) - this.selectionStartIndex = caret.charIndex - this.selectionEndIndex = caret.charIndex - this.updateSelection(textRenderInfo) - return caret - } - - clearSelection(){ - this.selectionStartIndex = 0 - this.selectionEndIndex = 0 - this.selectionRects = [] - this._domElSelectedText.textContent = '' - this.highlightText() - } - - /** - * Given a local x/y coordinate in the text block plane, set the end position of the caret - * used in text selection - * @param {number} x - * @param {number} y - * @return {TextCaret | null} - */ - moveCaret(textRenderInfo,x,y){ - let caret = getCaretAtPoint(textRenderInfo, x, y) - this.selectionEndIndex = caret.charIndex - this.updateSelection(textRenderInfo) - return caret - } - - /** - * update the selection visually and everything related to copy /paste - */ - updateSelection(textRenderInfo) { - this.selectedText = this.textMesh.text.substring(this.selectionStartIndex,this.selectionEndIndex) - this.selectionRects = getSelectionRects(textRenderInfo,this.selectionStartIndex,this.selectionEndIndex) - this._domElSelectedText.textContent = this.selectedText - this.highlightText() - this.selectDomText() - } - - /** - * Select the text contened in _domElSelectedText in order for it to reflect what's currently selected in the Text - */ - selectDomText(){ - const sel = document.getSelection() - sel.removeAllRanges() - const range = document.createRange() - range.selectNodeContents(this._domElSelectedText); //sets Range - sel.removeAllRanges(); //remove all ranges from selection - sel.addRange(range); - } - - /** - * update the position of the overlaying HTML that contain all the text that need to be accessible to screen readers - */ - updateDomPosition(renderer, camera) { - const {min, max} = this.textMesh.geometry.boundingBox - this._domElText.style.transform = this._textRectToCssMatrix(min.x, min.y, max.x, max.y, max.z, renderer, camera) - } - - /** - * update the position of the overlaying HTML that contain - * the selected text in order for it to be acessible through context menu copy - */ - updateSelectedDomPosition(renderer, camera) { - const rects = this.selectionRects - const el = this._domElSelectedText - if (rects && rects.length) { - // Find local space rect containing all selection rects - // TODO can we wrap this even tighter to multiline selections where top/bottom lines are partially selected? - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity - for (let i = 0; i < rects.length; i++) { - minX = Math.min(minX, rects[i].left) - minY = Math.min(minY, rects[i].bottom) - maxX = Math.max(maxX, rects[i].right) - maxY = Math.max(maxY, rects[i].top) - } - - const z = this.textMesh.geometry.boundingBox.max.z - el.style.transform = this._textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera) - el.style.display = 'block' - } else { - el.style.display = 'none' - } - } - - /** - * Given a rect in local text coordinates, build a CSS matrix3d that will transform - * a 10x10 DOM element to line up exactly with that rect on the screen. - * @private - */ - _textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera) { - const canvasRect = renderer.domElement.getBoundingClientRect() - - // element dimensions to geometry dimensions (flipping the y) - tempMat4a.makeScale((maxX - minX) / 10, (minY - maxY) / 10, 1) - .setPosition(tempVec3.set(minX, maxY, z)) - - // geometry to world - tempMat4a.premultiply(this.textMesh.matrixWorld) - - // world to camera - tempMat4a.premultiply(camera.matrixWorldInverse) - - // camera to projection - tempMat4a.premultiply(camera.projectionMatrix) - - // projection coords (-1 to 1) to screen pixels - tempMat4a.premultiply( - tempMat4b.makeScale(canvasRect.width / 2, -canvasRect.height / 2, 1) - .setPosition(canvasRect.left + canvasRect.width / 2, canvasRect.top + canvasRect.height / 2, 0) - ) - - return `matrix3d(${tempMat4a.elements.join(',')})` - } - - /** - * visually update the rendering of the text selection in the renderer context - */ - highlightText() { - - let THICKNESS = 0.25; - //todo manage rect update in a cleaner way. Currently we recreate everything everytime - //clean dispose of material no need to do it for geometry because we reuse the same - this.selectionRectsMeshs.forEach((rect)=>{ - if(rect.parent) - rect.parent.remove(rect) - rect.material.dispose() - }) - this.selectionRectsMeshs=[] - - this.selectionRects.forEach((rect)=>{ - let material = createDerivedMaterial( - this.textMesh.selectionMaterial ? this.textMesh.selectionMaterial : new MeshBasicMaterial({ - color:this.textMesh.selectionColor ? this.textMesh.selectionColor : defaultSelectionColor, - transparent: true, - opacity: 0.3, - depthWrite: false - }), - { - uniforms: { - rect: {value: new Vector4( - rect.left , - rect.top , - rect.right , - rect.bottom - )}, - depthAndCurveRadius: {value: new Vector2( - (rect.top - rect.bottom)*THICKNESS, - this.textMesh.curveRadius - )} - }, - vertexDefs: ` - uniform vec4 rect; - uniform vec2 depthAndCurveRadius; - `, - vertexTransform: ` - float depth = depthAndCurveRadius.x; - float rad = depthAndCurveRadius.y; - position.x = mix(rect.x, rect.z, position.x); - position.y = mix(rect.w, rect.y, position.y); - position.z = mix(-depth * 0.5, depth * 0.5, position.z); - if (rad != 0.0) { - float angle = position.x / rad; - position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); - // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); - } - ` - } - ) - material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] - let selectRect = new Mesh( - this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, - material - // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) - ) - this.selectionRectsMeshs.unshift(selectRect) - this.textMesh.add(selectRect) - }) - this.textMesh.updateWorldMatrix(false,true) - } - - updateHighlightTextUniforms(){ - if( - this.prevCurveRadius === 0 && this.textMesh.curveRadius !== 0 - || - this.prevCurveRadius !== 0 && this.textMesh.curveRadius === 0 - ){ - this.prevCurveRadius = this.textMesh.curveRadius - //update geometry - this.selectionRectsMeshs.forEach((rect)=>{ - rect.geometry = this.textMesh.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry - }) - } - this.selectionRectsMeshs.forEach((rect)=>{ - rect.material.uniforms.depthAndCurveRadius.value.y = this.textMesh.curveRadius - if(this.textMesh.selectionColor != rect.material.color){ - //faster to check fo color change or to set needsUpdate true each time ? - //todo - rect.material.color.set(this.textMesh.selectionColor) - rect.material.needsUpdate = true - } - }) - } - - /** - * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context - */ - startObservingMutation(){ - this.observer = new MutationObserver(this.mutationCallback.bind(this)); - this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); - } - - /** - * When a change occurs on the overlaying HTML, it reflect it in the renderer context - */ - mutationCallback(mutationsList, observer) { - this.currentHTML = this._domElText.innerHTML - this.textMesh.currentText = this.currentHTML.replace(/<(?!br\s*\/?)[^>]+>/g, '').replace(//gi, "\n"); - this.clearSelection() - this.textMesh._needsSync = true; - this.textMesh.sync() - } - - destroy(){ - this.clearSelection() - this.observer.disconnect() - this._domElText.remove() - this._domElSelectedText.remove() - this.childrenGeometry.dispose() - this.childrenCurvedGeometry.dispose() - } - - } - - return A11yManager -})() - -export { - A11yManager -} diff --git a/packages/troika-three-text/src/TextHighlighter.js b/packages/troika-three-text/src/TextHighlighter.js new file mode 100644 index 00000000..4ad4ed05 --- /dev/null +++ b/packages/troika-three-text/src/TextHighlighter.js @@ -0,0 +1,135 @@ +import { + Mesh, + MeshBasicMaterial, + Vector4, + Vector2, + BoxBufferGeometry +} from 'three' +import { createDerivedMaterial } from 'troika-three-utils' + +const defaultSelectionColor = 0xffffff + +const TextHighlighter = /*#__PURE__*/(() => { + + /** + * @class Text + * + * A ThreeJS Mesh that renders a string of text on a plane in 3D space using signed distance + * fields (SDF). + */ + class TextHighlighter { + constructor(textInstance, config = {}) { + const _config = Object.assign({ + thickness: 0.25 + }, config); + + this.textInstance = textInstance + + this.thickness = _config.thickness + this.prevCurveRadius = 0 + + this.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) + this.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1, 32).translate(0.5, 0.5, 0.5) + + } + + + /** + * visually update the rendering of the text selection in the renderer context + */ + highlightText() { + //todo manage rect update in a cleaner way. Currently we recreate everything everytime + //clean dispose of material no need to do it for geometry because we reuse the same + this.textInstance.selectionRectsMeshs.forEach((rect) => { + if (rect.parent) + rect.parent.remove(rect) + rect.material.dispose() + }) + this.textInstance.selectionRectsMeshs = [] + + this.textInstance.selectionRects.forEach((rect) => { + let material = createDerivedMaterial( + this.textInstance.selectionMaterial ? this.textInstance.selectionMaterial : new MeshBasicMaterial({ + color: this.textInstance.selectionColor ? this.textInstance.selectionColor : defaultSelectionColor, + transparent: true, + opacity: 0.3, + depthWrite: false + }), + { + uniforms: { + rect: { + value: new Vector4( + rect.left, + rect.top, + rect.right, + rect.bottom + ) + }, + depthAndCurveRadius: { + value: new Vector2( + (rect.top - rect.bottom) * this.thickness, + this.textInstance.curveRadius + ) + } + }, + vertexDefs: ` + uniform vec4 rect; + uniform vec2 depthAndCurveRadius; + `, + vertexTransform: ` + float depth = depthAndCurveRadius.x; + float rad = depthAndCurveRadius.y; + position.x = mix(rect.x, rect.z, position.x); + position.y = mix(rect.w, rect.y, position.y); + position.z = mix(-depth * 0.5, depth * 0.5, position.z); + if (rad != 0.0) { + float angle = position.x / rad; + position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); + // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); + } + ` + } + ) + material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] + let selectRect = new Mesh( + this.textInstance.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, + material + // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) + ) + this.textInstance.selectionRectsMeshs.unshift(selectRect) + this.textInstance.add(selectRect) + }) + this.textInstance.updateWorldMatrix(false, true) + } + + updateHighlightTextUniforms() { + if ( + this.prevCurveRadius === 0 && this.textInstance.curveRadius !== 0 + || + this.prevCurveRadius !== 0 && this.textInstance.curveRadius === 0 + ) { + this.prevCurveRadius = this.textInstance.curveRadius + //update geometry + this.textInstance.selectionRectsMeshs.forEach((rect) => { + rect.geometry = this.textInstance.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry + }) + } + this.textInstance.selectionRectsMeshs.forEach((rect) => { + rect.material.uniforms.depthAndCurveRadius.value.y = this.textInstance.curveRadius + if (this.textInstance.selectionColor != rect.material.color) { + //faster to check fo color change or to set needsUpdate true each time ? + //todo + rect.material.color.set(this.textInstance.selectionColor) + rect.material.needsUpdate = true + } + }) + } + + } + + return TextHighlighter +})() + +export { + TextHighlighter +} diff --git a/packages/troika-three-text/src/index.js b/packages/troika-three-text/src/index.js index 2554aebb..d0097cb7 100644 --- a/packages/troika-three-text/src/index.js +++ b/packages/troika-three-text/src/index.js @@ -7,3 +7,4 @@ export { createTextDerivedMaterial } from './TextDerivedMaterial.js' export { getCaretAtPoint, getSelectionRects } from './selectionUtils.js' export { makeDOMAcessible } from './makeDOMAcessible.js' export { makeSelectable } from './makeSelectable.js' +export { TextHighlighter } from './TextHighlighter.js' diff --git a/packages/troika-three-text/src/makeSelectable.js b/packages/troika-three-text/src/makeSelectable.js index a228fd53..b1cf80a4 100644 --- a/packages/troika-three-text/src/makeSelectable.js +++ b/packages/troika-three-text/src/makeSelectable.js @@ -1,14 +1,9 @@ import { Matrix4, - Mesh, - MeshBasicMaterial, - Vector4, Vector3, - Vector2, - BoxBufferGeometry } from 'three' -import { createDerivedMaterial } from 'troika-three-utils' import { getSelectionRects, getCaretAtPoint } from './selectionUtils' +import { TextHighlighter } from './TextHighlighter.js' const domOverlayBaseStyles = ` position:fixed; @@ -28,8 +23,6 @@ user-select: all; const makeSelectable = (textInstance, eventEmitter) => { - const defaultSelectionColor = 0xffffff - const tempMat4a = new Matrix4() const tempMat4b = new Matrix4() const tempVec3 = new Vector3() @@ -48,17 +41,15 @@ const makeSelectable = (textInstance, eventEmitter) => { textInstance.selectionRects = [] textInstance.selectionRectsMeshs = [] - textInstance.prevCurveRadius = 0 textInstance.isSelectable = true - textInstance.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) - textInstance.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1, 32).translate(0.5, 0.5, 0.5) - textInstance.addEventListener('syncstart', function () { if (!this.selectable && this.selectionRects.length != 0) this.clearSelection() }) + textInstance.TextHighlighter = new TextHighlighter(textInstance) + /** * Given a local x/y coordinate in the text block plane, set the start position of the caret * used in text selection @@ -79,7 +70,7 @@ const makeSelectable = (textInstance, eventEmitter) => { this.selectionEndIndex = 0 this.selectionRects = [] this._domElSelectedText.textContent = '' - this.highlightText() + this.TextHighlighter.highlightText() } /** @@ -103,7 +94,7 @@ const makeSelectable = (textInstance, eventEmitter) => { this.selectedText = this.text.substring(this.selectionStartIndex, this.selectionEndIndex) this.selectionRects = getSelectionRects(textRenderInfo, this.selectionStartIndex, this.selectionEndIndex) this._domElSelectedText.textContent = this.selectedText - this.highlightText() + this.TextHighlighter.highlightText() this.selectDomText() } @@ -124,7 +115,6 @@ const makeSelectable = (textInstance, eventEmitter) => { * the selected text in order for it to be acessible through context menu copy */ textInstance.updateSelectedDomPosition = function (renderer, camera) { - console.log('updateSelectedDomPosition') const rects = this.selectionRects const el = this._domElSelectedText if (rects && rects.length) { @@ -176,100 +166,8 @@ const makeSelectable = (textInstance, eventEmitter) => { return `matrix3d(${tempMat4a.elements.join(',')})` } - /** - * visually update the rendering of the text selection in the renderer context - */ - textInstance.highlightText = function () { - let THICKNESS = 0.25; - //todo manage rect update in a cleaner way. Currently we recreate everything everytime - //clean dispose of material no need to do it for geometry because we reuse the same - this.selectionRectsMeshs.forEach((rect) => { - if (rect.parent) - rect.parent.remove(rect) - rect.material.dispose() - }) - this.selectionRectsMeshs = [] - - this.selectionRects.forEach((rect) => { - let material = createDerivedMaterial( - this.selectionMaterial ? this.selectionMaterial : new MeshBasicMaterial({ - color: this.selectionColor ? this.selectionColor : defaultSelectionColor, - transparent: true, - opacity: 0.3, - depthWrite: false - }), - { - uniforms: { - rect: { - value: new Vector4( - rect.left, - rect.top, - rect.right, - rect.bottom - ) - }, - depthAndCurveRadius: { - value: new Vector2( - (rect.top - rect.bottom) * THICKNESS, - this.curveRadius - ) - } - }, - vertexDefs: ` - uniform vec4 rect; - uniform vec2 depthAndCurveRadius; - `, - vertexTransform: ` - float depth = depthAndCurveRadius.x; - float rad = depthAndCurveRadius.y; - position.x = mix(rect.x, rect.z, position.x); - position.y = mix(rect.w, rect.y, position.y); - position.z = mix(-depth * 0.5, depth * 0.5, position.z); - if (rad != 0.0) { - float angle = position.x / rad; - position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); - // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); - } - ` - } - ) - material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] - let selectRect = new Mesh( - this.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, - material - // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) - ) - this.selectionRectsMeshs.unshift(selectRect) - this.add(selectRect) - }) - this.updateWorldMatrix(false, true) - } - - textInstance.updateHighlightTextUniforms = function () { - if ( - this.prevCurveRadius === 0 && this.curveRadius !== 0 - || - this.prevCurveRadius !== 0 && this.curveRadius === 0 - ) { - this.prevCurveRadius = this.curveRadius - //update geometry - this.selectionRectsMeshs.forEach((rect) => { - rect.geometry = this.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry - }) - } - this.selectionRectsMeshs.forEach((rect) => { - rect.material.uniforms.depthAndCurveRadius.value.y = this.curveRadius - if (this.selectionColor != rect.material.color) { - //faster to check fo color change or to set needsUpdate true each time ? - //todo - rect.material.color.set(this.selectionColor) - rect.material.needsUpdate = true - } - }) - } - textInstance.addEventListener('beforerender', function () { - this.updateHighlightTextUniforms() + this.TextHighlighter.updateHighlightTextUniforms() }) textInstance.addEventListener('afterrender', function () { From af344c536c3d6af9cdcdf120a595eb980f354aa4 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Thu, 25 Mar 2021 13:01:05 +0100 Subject: [PATCH 20/27] TextHighlight as Group Subclass --- .../troika-three-text/src/TextHighlight.js | 132 ++++++++++++++++++ packages/troika-three-text/src/index.js | 2 +- .../troika-three-text/src/makeSelectable.js | 16 ++- 3 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 packages/troika-three-text/src/TextHighlight.js diff --git a/packages/troika-three-text/src/TextHighlight.js b/packages/troika-three-text/src/TextHighlight.js new file mode 100644 index 00000000..68a7a5a2 --- /dev/null +++ b/packages/troika-three-text/src/TextHighlight.js @@ -0,0 +1,132 @@ +import { + Mesh, + MeshBasicMaterial, + Vector4, + Vector2, + BoxBufferGeometry, + Group +} from 'three' +import { createDerivedMaterial } from 'troika-three-utils' + +const defaultSelectionColor = 0xffffff + +const TextHighlight = /*#__PURE__*/(() => { + + /** + * @class Text + * + * A ThreeJS Mesh that renders a string of text on a plane in 3D space using signed distance + * fields (SDF). + */ + class TextHighlight extends Group { + constructor(parent) { + super(parent) + + this.color = 'white' + this.startIndex = 0 + this.endIndex = 0 + this.thickness = 0.25 + this.prevCurveRadius = 0 + this.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) + this.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1, 32).translate(0.5, 0.5, 0.5) + } + + /** + * visually update the rendering of the text selection in the renderer context + */ + highlightText() { + //todo manage rect update in a cleaner way. Currently we recreate everything everytime + //clean dispose of material no need to do it for geometry because we reuse the same + this.parent.selectionRectsMeshs.forEach((rect) => { + if (rect.parent) + rect.parent.remove(rect) + rect.material.dispose() + }) + this.parent.selectionRectsMeshs = [] + + this.parent.selectionRects.forEach((rect) => { + let material = createDerivedMaterial( + this.parent.selectionMaterial ? this.parent.selectionMaterial : new MeshBasicMaterial({ + color: this.parent.selectionColor ? this.parent.selectionColor : defaultSelectionColor, + transparent: true, + opacity: 0.3, + depthWrite: false + }), + { + uniforms: { + rect: { + value: new Vector4( + rect.left, + rect.top, + rect.right, + rect.bottom + ) + }, + depthAndCurveRadius: { + value: new Vector2( + (rect.top - rect.bottom) * this.thickness, + this.parent.curveRadius + ) + } + }, + vertexDefs: ` + uniform vec4 rect; + uniform vec2 depthAndCurveRadius; + `, + vertexTransform: ` + float depth = depthAndCurveRadius.x; + float rad = depthAndCurveRadius.y; + position.x = mix(rect.x, rect.z, position.x); + position.y = mix(rect.w, rect.y, position.y); + position.z = mix(-depth * 0.5, depth * 0.5, position.z); + if (rad != 0.0) { + float angle = position.x / rad; + position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); + // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); + } + ` + } + ) + material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] + let selectRect = new Mesh( + this.parent.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, + material + // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) + ) + this.parent.selectionRectsMeshs.unshift(selectRect) + this.parent.add(selectRect) + }) + this.parent.updateWorldMatrix(false, true) + } + + updateHighlightTextUniforms() { + if ( + this.prevCurveRadius === 0 && this.parent.curveRadius !== 0 + || + this.prevCurveRadius !== 0 && this.parent.curveRadius === 0 + ) { + this.prevCurveRadius = this.parent.curveRadius + //update geometry + this.parent.selectionRectsMeshs.forEach((rect) => { + rect.geometry = this.parent.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry + }) + } + this.parent.selectionRectsMeshs.forEach((rect) => { + rect.material.uniforms.depthAndCurveRadius.value.y = this.parent.curveRadius + if (this.parent.selectionColor != rect.material.color) { + //faster to check fo color change or to set needsUpdate true each time ? + //todo + rect.material.color.set(this.parent.selectionColor) + rect.material.needsUpdate = true + } + }) + } + + } + + return TextHighlight +})() + +export { + TextHighlight +} diff --git a/packages/troika-three-text/src/index.js b/packages/troika-three-text/src/index.js index d0097cb7..6358daaf 100644 --- a/packages/troika-three-text/src/index.js +++ b/packages/troika-three-text/src/index.js @@ -7,4 +7,4 @@ export { createTextDerivedMaterial } from './TextDerivedMaterial.js' export { getCaretAtPoint, getSelectionRects } from './selectionUtils.js' export { makeDOMAcessible } from './makeDOMAcessible.js' export { makeSelectable } from './makeSelectable.js' -export { TextHighlighter } from './TextHighlighter.js' +export { TextHighlight } from './TextHighlight.js' diff --git a/packages/troika-three-text/src/makeSelectable.js b/packages/troika-three-text/src/makeSelectable.js index b1cf80a4..54ae5c3a 100644 --- a/packages/troika-three-text/src/makeSelectable.js +++ b/packages/troika-three-text/src/makeSelectable.js @@ -3,7 +3,7 @@ import { Vector3, } from 'three' import { getSelectionRects, getCaretAtPoint } from './selectionUtils' -import { TextHighlighter } from './TextHighlighter.js' +import { TextHighlight } from './TextHighlight.js' const domOverlayBaseStyles = ` position:fixed; @@ -48,7 +48,8 @@ const makeSelectable = (textInstance, eventEmitter) => { this.clearSelection() }) - textInstance.TextHighlighter = new TextHighlighter(textInstance) + textInstance.highlight = new TextHighlight() + textInstance.add(textInstance.highlight) /** * Given a local x/y coordinate in the text block plane, set the start position of the caret @@ -61,6 +62,8 @@ const makeSelectable = (textInstance, eventEmitter) => { let caret = getCaretAtPoint(textRenderInfo, x, y) this.selectionStartIndex = caret.charIndex this.selectionEndIndex = caret.charIndex + this.highlight.startIndex = caret.charIndex + this.highlight.endIndex = caret.charIndex this.updateSelection(textRenderInfo) return caret } @@ -68,9 +71,11 @@ const makeSelectable = (textInstance, eventEmitter) => { textInstance.clearSelection = function () { this.selectionStartIndex = 0 this.selectionEndIndex = 0 + this.highlight.startIndex = 0 + this.highlight.endIndex = 0 this.selectionRects = [] this._domElSelectedText.textContent = '' - this.TextHighlighter.highlightText() + this.highlight.highlightText() } /** @@ -83,6 +88,7 @@ const makeSelectable = (textInstance, eventEmitter) => { textInstance.moveCaret = function (textRenderInfo, x, y) { let caret = getCaretAtPoint(textRenderInfo, x, y) this.selectionEndIndex = caret.charIndex + this.highlight.endIndex = caret.charIndex this.updateSelection(textRenderInfo) return caret } @@ -94,7 +100,7 @@ const makeSelectable = (textInstance, eventEmitter) => { this.selectedText = this.text.substring(this.selectionStartIndex, this.selectionEndIndex) this.selectionRects = getSelectionRects(textRenderInfo, this.selectionStartIndex, this.selectionEndIndex) this._domElSelectedText.textContent = this.selectedText - this.TextHighlighter.highlightText() + this.highlight.highlightText() this.selectDomText() } @@ -167,7 +173,7 @@ const makeSelectable = (textInstance, eventEmitter) => { } textInstance.addEventListener('beforerender', function () { - this.TextHighlighter.updateHighlightTextUniforms() + this.highlight.updateHighlightTextUniforms() }) textInstance.addEventListener('afterrender', function () { From 6325ae07f091da92ab7afac8379849e4ef124baf Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sat, 27 Mar 2021 17:49:18 +0100 Subject: [PATCH 21/27] move props to makers --- .../src/facade/SelectionManagerFacade.js | 17 ++- .../troika-3d-text/src/facade/Text3DFacade.js | 2 - packages/troika-three-text/src/Text.js | 50 +------ .../troika-three-text/src/TextHighlighter.js | 135 ------------------ .../troika-three-text/src/makeDOMAcessible.js | 3 +- .../troika-three-text/src/makeSelectable.js | 75 ++++------ 6 files changed, 43 insertions(+), 239 deletions(-) delete mode 100644 packages/troika-three-text/src/TextHighlighter.js diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index f4a497fe..d779cd21 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -1,5 +1,6 @@ import { ListFacade } from 'troika-3d' import { Matrix4, Plane, Vector2, Vector3 } from 'three' +import { getCaretAtPoint, getSelectionRects } from 'troika-three-text' import { invertMatrix4 } from 'troika-three-utils' import SelectionRangeRect from './SelectionRangeRect.js' @@ -47,8 +48,11 @@ class SelectionManagerFacade extends ListFacade { const textRenderInfo = textMesh.textRenderInfo if (textRenderInfo) { const textPos = textMesh.worldPositionToTextCoords(e.intersection.point, tempVec2) - const caret = textMesh.startCaret(textRenderInfo, textPos.x, textPos.y) + const caret = getCaretAtPoint(textRenderInfo, textPos.x, textPos.y) if (caret) { + textMesh.selectionStartIndex = caret.charIndex + textMesh.selectionEndIndex = caret.charIndex + textMesh.updateSelection(textRenderInfo) onSelectionChange(caret.charIndex, caret.charIndex) parent.addEventListener('drag', onDrag) parent.addEventListener('dragend', onDragEnd) @@ -74,8 +78,10 @@ class SelectionManagerFacade extends ListFacade { // textPos = ray.intersectPlane(tempPlane.setComponents(0, 0, 1, 0), tempVec3) } if (textPos) { - const caret = textMesh.moveCaret(textRenderInfo, textPos.x, textPos.y) + const caret = getCaretAtPoint(textRenderInfo, textPos.x, textPos.y) if (caret) { + textMesh.selectionEndIndex = caret.charIndex + textMesh.updateSelection(textRenderInfo) onSelectionChange(this.selectionStart, caret.charIndex) } } @@ -97,7 +103,12 @@ class SelectionManagerFacade extends ListFacade { target = target.parent } while (target !== null) //clear selection - textMesh.clearSelection() + const textRenderInfo = textMesh.textRenderInfo + if (textRenderInfo) { + textMesh.selectionStartIndex = 0 + textMesh.selectionEndIndex = 0 + textMesh.updateSelection(textRenderInfo) + } } //clear selection if missed click diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index eee68dac..e4d375d3 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -37,8 +37,6 @@ const TEXT_MESH_PROPS = [ 'glyphGeometryDetail', 'sdfGlyphSize', 'debugSDF', - 'selectable', - 'accessible', 'selectionMaterial' ] diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 1534b32e..5500fe4f 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -49,7 +49,6 @@ const Text = /*#__PURE__*/(() => { return mesh } - const syncStartEvent = { type: 'syncstart' } const syncCompleteEvent = { type: 'synccomplete' } const beforeRenderEvent = { type: 'beforerender' } const afterRenderEvent = { type: 'afterrender' } @@ -68,9 +67,7 @@ const Text = /*#__PURE__*/(() => { 'anchorX', 'anchorY', 'colorRanges', - 'sdfGlyphSize', - 'selectable', - 'accessible' + 'sdfGlyphSize' ] const COPYABLE_PROPS = SYNCABLE_PROPS.concat( @@ -375,50 +372,6 @@ const Text = /*#__PURE__*/(() => { */ this.sdfGlyphSize = null - // === dom related properties === // - - /** - * @member {boolean} selectable - * Defines whether the displayed text can be selected by the user in order to be copied to its - * clipboard for instance. This will automatically enable the accessible mode. - */ - this.selectable = false - - /** - * @member {boolean} accessible - * Defines whether the displayed text can be read by screen readers or not. - * Enabling this also allow the user to visually translate the text using browser translating tools. - * This setting use overlaying HTML that is kept above the rendered text. - */ - this.accessible = false - - /** - * @member {THREE.Material} selectionMaterial - * Defines a _base_ material to be used when rendering the text selection. This material will be - * automatically replaced with a material derived from it, that adds shader code to manage - * curved text. - * By default it will derive from a simple white MeshBasicMaterial with alpha of 0.3, but you can use any - * of the other mesh materials to gain other features like lighting, texture maps, etc. - * - * Also see the `selectionColor` shortcut property. - */ - this.selectionMaterial = null - - /** - * @member {string|number|THREE.Color} selectionColor - * This is a shortcut for setting the `color` of the text selection's material. You can use this - * if you don't want to specify a whole custom `material`. Also, if you do use a custom - * `material`, this color will only be used for this particuar Text instance, even if - * that same material instance is shared across multiple Text objects. - */ - this.selectionColor = null - - /** - * @member {element} domContainer - * When set, the overlaying HTML of the selection or accessible feature will be a child of this container. - */ - this.domContainer = null - this.debugSDF = false } @@ -437,7 +390,6 @@ const Text = /*#__PURE__*/(() => { (this._queuedSyncs || (this._queuedSyncs = [])).push(callback) } else { this._isSyncing = true - this.dispatchEvent(syncStartEvent) this.currentText = this.currentText ? this.currentText : this.text diff --git a/packages/troika-three-text/src/TextHighlighter.js b/packages/troika-three-text/src/TextHighlighter.js deleted file mode 100644 index 4ad4ed05..00000000 --- a/packages/troika-three-text/src/TextHighlighter.js +++ /dev/null @@ -1,135 +0,0 @@ -import { - Mesh, - MeshBasicMaterial, - Vector4, - Vector2, - BoxBufferGeometry -} from 'three' -import { createDerivedMaterial } from 'troika-three-utils' - -const defaultSelectionColor = 0xffffff - -const TextHighlighter = /*#__PURE__*/(() => { - - /** - * @class Text - * - * A ThreeJS Mesh that renders a string of text on a plane in 3D space using signed distance - * fields (SDF). - */ - class TextHighlighter { - constructor(textInstance, config = {}) { - const _config = Object.assign({ - thickness: 0.25 - }, config); - - this.textInstance = textInstance - - this.thickness = _config.thickness - this.prevCurveRadius = 0 - - this.childrenGeometry = new BoxBufferGeometry(1, 1, 0.1).translate(0.5, 0.5, 0.5) - this.childrenCurvedGeometry = new BoxBufferGeometry(1, 1, 0.1, 32).translate(0.5, 0.5, 0.5) - - } - - - /** - * visually update the rendering of the text selection in the renderer context - */ - highlightText() { - //todo manage rect update in a cleaner way. Currently we recreate everything everytime - //clean dispose of material no need to do it for geometry because we reuse the same - this.textInstance.selectionRectsMeshs.forEach((rect) => { - if (rect.parent) - rect.parent.remove(rect) - rect.material.dispose() - }) - this.textInstance.selectionRectsMeshs = [] - - this.textInstance.selectionRects.forEach((rect) => { - let material = createDerivedMaterial( - this.textInstance.selectionMaterial ? this.textInstance.selectionMaterial : new MeshBasicMaterial({ - color: this.textInstance.selectionColor ? this.textInstance.selectionColor : defaultSelectionColor, - transparent: true, - opacity: 0.3, - depthWrite: false - }), - { - uniforms: { - rect: { - value: new Vector4( - rect.left, - rect.top, - rect.right, - rect.bottom - ) - }, - depthAndCurveRadius: { - value: new Vector2( - (rect.top - rect.bottom) * this.thickness, - this.textInstance.curveRadius - ) - } - }, - vertexDefs: ` - uniform vec4 rect; - uniform vec2 depthAndCurveRadius; - `, - vertexTransform: ` - float depth = depthAndCurveRadius.x; - float rad = depthAndCurveRadius.y; - position.x = mix(rect.x, rect.z, position.x); - position.y = mix(rect.w, rect.y, position.y); - position.z = mix(-depth * 0.5, depth * 0.5, position.z); - if (rad != 0.0) { - float angle = position.x / rad; - position.xz = vec2(sin(angle) * (rad - position.z), rad - cos(angle) * (rad - position.z)); - // TODO fix normals: normal.xz = vec2(sin(angle), cos(angle)); - } - ` - } - ) - material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] - let selectRect = new Mesh( - this.textInstance.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry, - material - // new MeshBasicMaterial({color: 0xffffff,side: DoubleSide,transparent: true, opacity:0.5}) - ) - this.textInstance.selectionRectsMeshs.unshift(selectRect) - this.textInstance.add(selectRect) - }) - this.textInstance.updateWorldMatrix(false, true) - } - - updateHighlightTextUniforms() { - if ( - this.prevCurveRadius === 0 && this.textInstance.curveRadius !== 0 - || - this.prevCurveRadius !== 0 && this.textInstance.curveRadius === 0 - ) { - this.prevCurveRadius = this.textInstance.curveRadius - //update geometry - this.textInstance.selectionRectsMeshs.forEach((rect) => { - rect.geometry = this.textInstance.curveRadius === 0 ? this.childrenGeometry : this.childrenCurvedGeometry - }) - } - this.textInstance.selectionRectsMeshs.forEach((rect) => { - rect.material.uniforms.depthAndCurveRadius.value.y = this.textInstance.curveRadius - if (this.textInstance.selectionColor != rect.material.color) { - //faster to check fo color change or to set needsUpdate true each time ? - //todo - rect.material.color.set(this.textInstance.selectionColor) - rect.material.needsUpdate = true - } - }) - } - - } - - return TextHighlighter -})() - -export { - TextHighlighter -} diff --git a/packages/troika-three-text/src/makeDOMAcessible.js b/packages/troika-three-text/src/makeDOMAcessible.js index ee7bb960..d92e4355 100644 --- a/packages/troika-three-text/src/makeDOMAcessible.js +++ b/packages/troika-three-text/src/makeDOMAcessible.js @@ -40,8 +40,7 @@ const makeDOMAcessible = (textInstance, options = {}) => { textInstance._domElText = document.createElement(_options.tagName) - textInstance.domContainer = _options.domContainer - textInstance.domContainer.appendChild(textInstance._domElText) + _options.domContainer.appendChild(textInstance._domElText) textInstance._domElText.style = domOverlayBaseStyles + domSRoutline textInstance.isDOMAccessible = true diff --git a/packages/troika-three-text/src/makeSelectable.js b/packages/troika-three-text/src/makeSelectable.js index 54ae5c3a..9f74b66a 100644 --- a/packages/troika-three-text/src/makeSelectable.js +++ b/packages/troika-three-text/src/makeSelectable.js @@ -21,19 +21,22 @@ line-height: 10px; user-select: all; ` -const makeSelectable = (textInstance, eventEmitter) => { +const makeSelectable = (textInstance, options = {}) => { const tempMat4a = new Matrix4() const tempMat4b = new Matrix4() const tempVec3 = new Vector3() + const _options = Object.assign({ + domContainer: document.documentElement + }, options); + textInstance._domElSelectedText = document.createElement('p') textInstance.selectionStartIndex = 0; textInstance.selectionEndIndex = 0; textInstance.selectedText = null; - textInstance.domContainer = document.documentElement - textInstance.domContainer.appendChild(textInstance._domElSelectedText) + _options.domContainer.appendChild(textInstance._domElSelectedText) textInstance._domElSelectedText.setAttribute('aria-hidden', 'true') textInstance._domElSelectedText.style = domOverlayBaseStyles @@ -43,55 +46,29 @@ const makeSelectable = (textInstance, eventEmitter) => { textInstance.isSelectable = true - textInstance.addEventListener('syncstart', function () { - if (!this.selectable && this.selectionRects.length != 0) - this.clearSelection() - }) - - textInstance.highlight = new TextHighlight() - textInstance.add(textInstance.highlight) - /** - * Given a local x/y coordinate in the text block plane, set the start position of the caret - * used in text selection - * @param {number} x - * @param {number} y - * @return {TextCaret | null} - */ - textInstance.startCaret = function (textRenderInfo, x, y) { - let caret = getCaretAtPoint(textRenderInfo, x, y) - this.selectionStartIndex = caret.charIndex - this.selectionEndIndex = caret.charIndex - this.highlight.startIndex = caret.charIndex - this.highlight.endIndex = caret.charIndex - this.updateSelection(textRenderInfo) - return caret - } - - textInstance.clearSelection = function () { - this.selectionStartIndex = 0 - this.selectionEndIndex = 0 - this.highlight.startIndex = 0 - this.highlight.endIndex = 0 - this.selectionRects = [] - this._domElSelectedText.textContent = '' - this.highlight.highlightText() - } + * @member {THREE.Material} selectionMaterial + * Defines a _base_ material to be used when rendering the text selection. This material will be + * automatically replaced with a material derived from it, that adds shader code to manage + * curved text. + * By default it will derive from a simple white MeshBasicMaterial with alpha of 0.3, but you can use any + * of the other mesh materials to gain other features like lighting, texture maps, etc. + * + * Also see the `selectionColor` shortcut property. + */ + textInstance.selectionMaterial = null /** - * Given a local x/y coordinate in the text block plane, set the end position of the caret - * used in text selection - * @param {number} x - * @param {number} y - * @return {TextCaret | null} + * @member {string|number|THREE.Color} selectionColor + * This is a shortcut for setting the `color` of the text selection's material. You can use this + * if you don't want to specify a whole custom `material`. Also, if you do use a custom + * `material`, this color will only be used for this particuar Text instance, even if + * that same material instance is shared across multiple Text objects. */ - textInstance.moveCaret = function (textRenderInfo, x, y) { - let caret = getCaretAtPoint(textRenderInfo, x, y) - this.selectionEndIndex = caret.charIndex - this.highlight.endIndex = caret.charIndex - this.updateSelection(textRenderInfo) - return caret - } + textInstance.selectionColor = null + + textInstance.highlight = new TextHighlight() + textInstance.add(textInstance.highlight) /** * update the selection visually and everything related to copy /paste @@ -100,6 +77,8 @@ const makeSelectable = (textInstance, eventEmitter) => { this.selectedText = this.text.substring(this.selectionStartIndex, this.selectionEndIndex) this.selectionRects = getSelectionRects(textRenderInfo, this.selectionStartIndex, this.selectionEndIndex) this._domElSelectedText.textContent = this.selectedText + this.highlight.startIndex = this.selectionStartIndex + this.highlight.endIndex = this.selectionEndIndex this.highlight.highlightText() this.selectDomText() } From 04c4c8c24e7145195500a9f41a5e0025b70c0e94 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sat, 27 Mar 2021 18:20:09 +0100 Subject: [PATCH 22/27] move textRectToCssMatrix to selectionutils --- .../troika-three-text/src/makeDOMAcessible.js | 42 ++-------------- .../troika-three-text/src/makeSelectable.js | 42 +--------------- .../troika-three-text/src/selectionUtils.js | 48 +++++++++++++++++-- 3 files changed, 48 insertions(+), 84 deletions(-) diff --git a/packages/troika-three-text/src/makeDOMAcessible.js b/packages/troika-three-text/src/makeDOMAcessible.js index d92e4355..09f22469 100644 --- a/packages/troika-three-text/src/makeDOMAcessible.js +++ b/packages/troika-three-text/src/makeDOMAcessible.js @@ -1,7 +1,5 @@ -import { - Matrix4, - Vector3, -} from 'three' +import { textRectToCssMatrix } from './selectionUtils' + const domOverlayBaseStyles = ` position:fixed; @@ -28,10 +26,6 @@ align-items: center; const makeDOMAcessible = (textInstance, options = {}) => { - const tempMat4a = new Matrix4() - const tempMat4b = new Matrix4() - const tempVec3 = new Vector3() - const _options = Object.assign({ domContainer: document.documentElement, tagName: 'p', @@ -101,7 +95,7 @@ const makeDOMAcessible = (textInstance, options = {}) => { textInstance.updateDomPosition = function (renderer, camera) { if (!this.pauseDomSync) { const { min, max } = this.geometry.boundingBox - this._domElText.style.transform = this._textRectToCssMatrix(min.x, min.y, max.x, max.y, max.z, renderer, camera) + this._domElText.style.transform = textRectToCssMatrix(min.x, min.y, max.x, max.y, max.z, renderer, camera, this.matrixWorld) } } @@ -111,36 +105,6 @@ const makeDOMAcessible = (textInstance, options = {}) => { this.updateDomPosition(renderer, camera) }) - /** - * Given a rect in local text coordinates, build a CSS matrix3d that will transform - * a 10x10 DOM element to line up exactly with that rect on the screen. - * @private - */ - textInstance._textRectToCssMatrix = function (minX, minY, maxX, maxY, z, renderer, camera) { - const canvasRect = renderer.domElement.getBoundingClientRect() - - // element dimensions to geometry dimensions (flipping the y) - tempMat4a.makeScale((maxX - minX) / 10, (minY - maxY) / 10, 1) - .setPosition(tempVec3.set(minX, maxY, z)) - - // geometry to world - tempMat4a.premultiply(this.matrixWorld) - - // world to camera - tempMat4a.premultiply(camera.matrixWorldInverse) - - // camera to projection - tempMat4a.premultiply(camera.projectionMatrix) - - // projection coords (-1 to 1) to screen pixels - tempMat4a.premultiply( - tempMat4b.makeScale(canvasRect.width / 2, -canvasRect.height / 2, 1) - .setPosition(canvasRect.left + canvasRect.width / 2, canvasRect.top + canvasRect.height / 2, 0) - ) - - return `matrix3d(${tempMat4a.elements.join(',')})` - } - textInstance.pause = function () { this.pauseDomSync = true } diff --git a/packages/troika-three-text/src/makeSelectable.js b/packages/troika-three-text/src/makeSelectable.js index 9f74b66a..05c1d0e0 100644 --- a/packages/troika-three-text/src/makeSelectable.js +++ b/packages/troika-three-text/src/makeSelectable.js @@ -1,8 +1,4 @@ -import { - Matrix4, - Vector3, -} from 'three' -import { getSelectionRects, getCaretAtPoint } from './selectionUtils' +import { getSelectionRects, textRectToCssMatrix } from './selectionUtils' import { TextHighlight } from './TextHighlight.js' const domOverlayBaseStyles = ` @@ -23,10 +19,6 @@ user-select: all; const makeSelectable = (textInstance, options = {}) => { - const tempMat4a = new Matrix4() - const tempMat4b = new Matrix4() - const tempVec3 = new Vector3() - const _options = Object.assign({ domContainer: document.documentElement }, options); @@ -114,43 +106,13 @@ const makeSelectable = (textInstance, options = {}) => { } const z = this.geometry.boundingBox.max.z - el.style.transform = this._textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera) + el.style.transform = textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera, this.matrixWorld) el.style.display = 'block' } else { el.style.display = 'none' } } - /** - * Given a rect in local text coordinates, build a CSS matrix3d that will transform - * a 10x10 DOM element to line up exactly with that rect on the screen. - * @private - */ - textInstance._textRectToCssMatrix = function (minX, minY, maxX, maxY, z, renderer, camera) { - const canvasRect = renderer.domElement.getBoundingClientRect() - - // element dimensions to geometry dimensions (flipping the y) - tempMat4a.makeScale((maxX - minX) / 10, (minY - maxY) / 10, 1) - .setPosition(tempVec3.set(minX, maxY, z)) - - // geometry to world - tempMat4a.premultiply(this.matrixWorld) - - // world to camera - tempMat4a.premultiply(camera.matrixWorldInverse) - - // camera to projection - tempMat4a.premultiply(camera.projectionMatrix) - - // projection coords (-1 to 1) to screen pixels - tempMat4a.premultiply( - tempMat4b.makeScale(canvasRect.width / 2, -canvasRect.height / 2, 1) - .setPosition(canvasRect.left + canvasRect.width / 2, canvasRect.top + canvasRect.height / 2, 0) - ) - - return `matrix3d(${tempMat4a.elements.join(',')})` - } - textInstance.addEventListener('beforerender', function () { this.highlight.updateHighlightTextUniforms() }) diff --git a/packages/troika-three-text/src/selectionUtils.js b/packages/troika-three-text/src/selectionUtils.js index 526c52cf..724992c7 100644 --- a/packages/troika-three-text/src/selectionUtils.js +++ b/packages/troika-three-text/src/selectionUtils.js @@ -1,3 +1,7 @@ +import { + Matrix4, + Vector3, +} from 'three' //=== Utility functions for dealing with carets and selection ranges ===// /** @@ -9,6 +13,10 @@ * character; the caret will be for the position _before_ that character. */ +const tempMat4a = new Matrix4() +const tempMat4b = new Matrix4() +const tempVec3 = new Vector3() + /** * Given a local x/y coordinate in the text block plane, find the nearest caret position. * @param {TroikaTextRenderInfo} textRenderInfo - a result object from TextBuilder#getTextRenderInfo @@ -18,7 +26,7 @@ */ export function getCaretAtPoint(textRenderInfo, x, y) { let closestCaret = null - const {caretHeight} = textRenderInfo + const { caretHeight } = textRenderInfo const caretsByRow = groupCaretsByRow(textRenderInfo) // Find nearest row by y first @@ -58,7 +66,7 @@ export function getSelectionRects(textRenderInfo, start, end) { return prevResult.rects } - const {caretPositions, caretHeight} = textRenderInfo + const { caretPositions, caretHeight } = textRenderInfo // Normalize if (end < start) { @@ -77,7 +85,7 @@ export function getSelectionRects(textRenderInfo, start, end) { const y = caretPositions[i * 3 + 2] let row = rows.get(y) if (!row) { - row = {left: x1, right: x2, bottom: y, top: y + caretHeight} + row = { left: x1, right: x2, bottom: y, top: y + caretHeight } rows.set(y, row) } else { row.left = Math.min(row.left, x1) @@ -89,7 +97,7 @@ export function getSelectionRects(textRenderInfo, start, end) { rects.push(rect) }) - _rectsCache.set(textRenderInfo, {start, end, rects}) + _rectsCache.set(textRenderInfo, { start, end, rects }) } return rects } @@ -100,7 +108,7 @@ function groupCaretsByRow(textRenderInfo) { // textRenderInfo is frozen so it's safe to cache based on it let caretsByRow = _caretsByRowCache.get(textRenderInfo) if (!caretsByRow) { - const {caretPositions, caretHeight} = textRenderInfo + const { caretPositions, caretHeight } = textRenderInfo caretsByRow = new Map() for (let i = 0; i < caretPositions.length; i += 3) { const rowY = caretPositions[i + 2] @@ -128,3 +136,33 @@ function groupCaretsByRow(textRenderInfo) { _caretsByRowCache.set(textRenderInfo, caretsByRow) return caretsByRow } + +/** + * Given a rect in local text coordinates, build a CSS matrix3d that will transform + * a 10x10 DOM element to line up exactly with that rect on the screen. + * @private + */ +export function textRectToCssMatrix(minX, minY, maxX, maxY, z, renderer, camera, matrixWorld) { + const canvasRect = renderer.domElement.getBoundingClientRect() + + // element dimensions to geometry dimensions (flipping the y) + tempMat4a.makeScale((maxX - minX) / 10, (minY - maxY) / 10, 1) + .setPosition(tempVec3.set(minX, maxY, z)) + + // geometry to world + tempMat4a.premultiply(matrixWorld) + + // world to camera + tempMat4a.premultiply(camera.matrixWorldInverse) + + // camera to projection + tempMat4a.premultiply(camera.projectionMatrix) + + // projection coords (-1 to 1) to screen pixels + tempMat4a.premultiply( + tempMat4b.makeScale(canvasRect.width / 2, -canvasRect.height / 2, 1) + .setPosition(canvasRect.left + canvasRect.width / 2, canvasRect.top + canvasRect.height / 2, 0) + ) + + return `matrix3d(${tempMat4a.elements.join(',')})` +} \ No newline at end of file From 63d311587999965996e4fd168de893e27d0bbc86 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sat, 27 Mar 2021 18:39:12 +0100 Subject: [PATCH 23/27] selection managed by TextHighligh, exposed by makeSelectable --- .../src/facade/SelectionManagerFacade.js | 13 ++++++------- packages/troika-three-text/src/makeSelectable.js | 8 ++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js index d779cd21..bc9b863c 100644 --- a/packages/troika-3d-text/src/facade/SelectionManagerFacade.js +++ b/packages/troika-3d-text/src/facade/SelectionManagerFacade.js @@ -1,7 +1,6 @@ import { ListFacade } from 'troika-3d' import { Matrix4, Plane, Vector2, Vector3 } from 'three' -import { getCaretAtPoint, getSelectionRects } from 'troika-three-text' -import { invertMatrix4 } from 'troika-three-utils' +import { getCaretAtPoint } from 'troika-three-text' import SelectionRangeRect from './SelectionRangeRect.js' const THICKNESS = 0.25 //rect depth as percentage of height @@ -50,8 +49,8 @@ class SelectionManagerFacade extends ListFacade { const textPos = textMesh.worldPositionToTextCoords(e.intersection.point, tempVec2) const caret = getCaretAtPoint(textRenderInfo, textPos.x, textPos.y) if (caret) { - textMesh.selectionStartIndex = caret.charIndex - textMesh.selectionEndIndex = caret.charIndex + textMesh.highlight.startIndex = caret.charIndex + textMesh.highlight.endIndex = caret.charIndex textMesh.updateSelection(textRenderInfo) onSelectionChange(caret.charIndex, caret.charIndex) parent.addEventListener('drag', onDrag) @@ -80,7 +79,7 @@ class SelectionManagerFacade extends ListFacade { if (textPos) { const caret = getCaretAtPoint(textRenderInfo, textPos.x, textPos.y) if (caret) { - textMesh.selectionEndIndex = caret.charIndex + textMesh.highlight.endIndex = caret.charIndex textMesh.updateSelection(textRenderInfo) onSelectionChange(this.selectionStart, caret.charIndex) } @@ -105,8 +104,8 @@ class SelectionManagerFacade extends ListFacade { //clear selection const textRenderInfo = textMesh.textRenderInfo if (textRenderInfo) { - textMesh.selectionStartIndex = 0 - textMesh.selectionEndIndex = 0 + textMesh.highlight.startIndex = 0 + textMesh.highlight.endIndex = 0 textMesh.updateSelection(textRenderInfo) } } diff --git a/packages/troika-three-text/src/makeSelectable.js b/packages/troika-three-text/src/makeSelectable.js index 05c1d0e0..9b671a89 100644 --- a/packages/troika-three-text/src/makeSelectable.js +++ b/packages/troika-three-text/src/makeSelectable.js @@ -24,8 +24,6 @@ const makeSelectable = (textInstance, options = {}) => { }, options); textInstance._domElSelectedText = document.createElement('p') - textInstance.selectionStartIndex = 0; - textInstance.selectionEndIndex = 0; textInstance.selectedText = null; _options.domContainer.appendChild(textInstance._domElSelectedText) @@ -66,11 +64,9 @@ const makeSelectable = (textInstance, options = {}) => { * update the selection visually and everything related to copy /paste */ textInstance.updateSelection = function (textRenderInfo) { - this.selectedText = this.text.substring(this.selectionStartIndex, this.selectionEndIndex) - this.selectionRects = getSelectionRects(textRenderInfo, this.selectionStartIndex, this.selectionEndIndex) + this.selectedText = this.text.substring(this.highlight.startIndex, this.highlight.endIndex) + this.selectionRects = getSelectionRects(textRenderInfo, this.highlight.startIndex, this.highlight.endIndex) this._domElSelectedText.textContent = this.selectedText - this.highlight.startIndex = this.selectionStartIndex - this.highlight.endIndex = this.selectionEndIndex this.highlight.highlightText() this.selectDomText() } From 163d610b7ab3ee06fc6b4fb0a11ee65eeb038df8 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sun, 4 Apr 2021 19:07:02 +0200 Subject: [PATCH 24/27] fix selectionMaterial --- packages/troika-three-text/src/TextHighlight.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/troika-three-text/src/TextHighlight.js b/packages/troika-three-text/src/TextHighlight.js index 68a7a5a2..adc2a6c4 100644 --- a/packages/troika-three-text/src/TextHighlight.js +++ b/packages/troika-three-text/src/TextHighlight.js @@ -46,7 +46,7 @@ const TextHighlight = /*#__PURE__*/(() => { this.parent.selectionRects.forEach((rect) => { let material = createDerivedMaterial( - this.parent.selectionMaterial ? this.parent.selectionMaterial : new MeshBasicMaterial({ + this.parent.selectionMaterial ? this.parent.selectionMaterial.clone() : new MeshBasicMaterial({ color: this.parent.selectionColor ? this.parent.selectionColor : defaultSelectionColor, transparent: true, opacity: 0.3, From 696d6ab4a0b134903762a17e55ec616dd32afcd3 Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sun, 4 Apr 2021 20:04:14 +0200 Subject: [PATCH 25/27] fix a11y translation and text update --- .../troika-3d-text/src/facade/Text3DFacade.js | 7 ------ packages/troika-three-text/src/Text.js | 8 ++++++ .../troika-three-text/src/makeDOMAcessible.js | 25 ++++++------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index e4d375d3..aa36dad0 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -101,13 +101,6 @@ class Text3DFacade extends Object3DFacade { makeSelectable(this.threeObject) } - if (this.text !== this._prevText) { - // TODO mirror to DOM... this._domEl.textContent = this.text - // Clear selection when text changes - this.selectionStart = this.selectionEnd = -1 - this._prevText = this.text - } - this._updateSelection() } diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 5500fe4f..880d77e6 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -50,6 +50,7 @@ const Text = /*#__PURE__*/(() => { } const syncCompleteEvent = { type: 'synccomplete' } + const textChangeEvent = { type: 'textChange' } const beforeRenderEvent = { type: 'beforerender' } const afterRenderEvent = { type: 'afterrender' } @@ -385,6 +386,13 @@ const Text = /*#__PURE__*/(() => { if (this._needsSync) { this._needsSync = false + /* detect text change coming from the component */ + if (this.prevText !== this.text) { + this.currentText = this.text + this.dispatchEvent(textChangeEvent) + this.prevText = this.text + } + // If there's another sync still in progress, queue if (this._isSyncing) { (this._queuedSyncs || (this._queuedSyncs = [])).push(callback) diff --git a/packages/troika-three-text/src/makeDOMAcessible.js b/packages/troika-three-text/src/makeDOMAcessible.js index 09f22469..fcc61ac8 100644 --- a/packages/troika-three-text/src/makeDOMAcessible.js +++ b/packages/troika-three-text/src/makeDOMAcessible.js @@ -67,25 +67,16 @@ const makeDOMAcessible = (textInstance, options = {}) => { textInstance.pauseDomSync = false textInstance.syncDOM = function () { - if (this.prevText !== this.text) { - this.currentText = this.text - this.prevHTML = this.currentHTML - this.currentHTML = this.text.replace(/(?:\r\n|\r|\n)/g, '
') - this.prevText = this.text - } - - this.currentText = this.currentText ? this.currentText : this.text - - //update dom with latest text - if (this.prevHTML !== this.currentHTML) { - this.observer.disconnect() - this._domElText.innerHTML = this.currentHTML; - this.prevHTML = this.currentHTML - this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); - } + this.currentText = this.text + this.prevText = this.text + this.currentHTML = this.text.replace(/(?:\r\n|\r|\n)/g, '
') + this.observer.disconnect() + this._domElText.innerHTML = this.currentHTML; + this.prevHTML = this.currentHTML + this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); } - textInstance.addEventListener('syncstart', textInstance.syncDOM) + textInstance.addEventListener('textChange', textInstance.syncDOM) textInstance.syncDOM() From 37690518420bfa72fbd5897336a324156270aa9d Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Sun, 4 Apr 2021 21:09:23 +0200 Subject: [PATCH 26/27] make observeMutation optional --- packages/troika-three-text/src/makeDOMAcessible.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/troika-three-text/src/makeDOMAcessible.js b/packages/troika-three-text/src/makeDOMAcessible.js index fcc61ac8..5ada5554 100644 --- a/packages/troika-three-text/src/makeDOMAcessible.js +++ b/packages/troika-three-text/src/makeDOMAcessible.js @@ -25,6 +25,7 @@ align-items: center; ` const makeDOMAcessible = (textInstance, options = {}) => { + console.log(options) const _options = Object.assign({ domContainer: document.documentElement, @@ -49,6 +50,7 @@ const makeDOMAcessible = (textInstance, options = {}) => { } if (_options.observeMutation) { + console.log(_options.observeMutation) /** * Start watching change on the overlaying HTML such as browser dom translation in order to reflect it in the renderer context */ @@ -70,10 +72,12 @@ const makeDOMAcessible = (textInstance, options = {}) => { this.currentText = this.text this.prevText = this.text this.currentHTML = this.text.replace(/(?:\r\n|\r|\n)/g, '
') - this.observer.disconnect() + if (_options.observeMutation) + this.observer.disconnect() this._domElText.innerHTML = this.currentHTML; this.prevHTML = this.currentHTML - this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); + if (_options.observeMutation) + this.observer.observe(this._domElText, { attributes: false, childList: true, subtree: false }); } textInstance.addEventListener('textChange', textInstance.syncDOM) From 16fdca6704f568cc7ef52f5464addde4bdad4bae Mon Sep 17 00:00:00 2001 From: AlaricBaraou Date: Tue, 6 Apr 2021 13:13:13 +0200 Subject: [PATCH 27/27] fix merge conflict --- packages/troika-three-text/src/selectionUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/troika-three-text/src/selectionUtils.js b/packages/troika-three-text/src/selectionUtils.js index 724992c7..e9c38852 100644 --- a/packages/troika-three-text/src/selectionUtils.js +++ b/packages/troika-three-text/src/selectionUtils.js @@ -85,7 +85,7 @@ export function getSelectionRects(textRenderInfo, start, end) { const y = caretPositions[i * 3 + 2] let row = rows.get(y) if (!row) { - row = { left: x1, right: x2, bottom: y, top: y + caretHeight } + row = { left: Math.min(x1, x2), right: Math.max(x1, x2), bottom: y, top: y + caretHeight } rows.set(y, row) } else { row.left = Math.min(row.left, x1)