-
Notifications
You must be signed in to change notification settings - Fork 125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP - Selection Copy + a11y features #110
base: main
Are you sure you want to change the base?
Conversation
I haven't gone through in detail yet but have some responses to specific items:
The current approach seems fine to me, what is your reasoning for delegating to frameworks? Is there a performance impact?
In Troika this can be done by adding an event listener to the scene (
Interesting... in what way is the intersection wrong? The intent is for the dragstart event to carry the location of the mousedown, but not fire until after the mouse has been moved a short distance. I think your proposed change would make the dragstart have the location after the first move, which isn't what we want. You could also just use mousedown/mousemove events directly, if the dragstart buffering gets in your way.
Nice. :) Even more flexible would be
It looks like you need to project all 4 corners rather than just the 2 min/max corners...? If you wanted to get really fancy you could apply a 3d css transform to match the exact perspective. ;) Kinda like drei's Html component can do. |
import { invertMatrix4 } from 'troika-three-utils' | ||
import SelectionRangeRect from './SelectionRangeRect.js' | ||
import { Mesh } from '../../../../node_modules/three/src/Three.js' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the source of your Netlify build error, btw.
Aha! I tracked down this issue, it's a mutation bug on my part. It stems from this in SelectionManager.js: parent.addEventListener('dragstart', onDragStart)
parent.addEventListener('mousedown', onDragStart) So onDragStart is being fired twice, once on mousedown and then slightly later on dragstart. (I don't exactly remember why I'm listening to both, but that's another issue) The way I implemented the dragstart event handling, it uses the same intersection object as the mousedown. So when onDragStart fires twice, it gets the same intersection for both. But then in onDragStart it calls I've fixed that here: d487b8a - if you merge that into your branch it should fix your issue.
Ignore the geometry's position attribute, that's just a 1x1 plane which is transformed for each glyph in the shader. You want the bounding box, which you did here: 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); I think you're on the right track here, but you need to project all the bounding box's corners (4 when flat, 8 when curved), and then take the min/max from the projected screen coords. |
I think I'm good with all of the above. Regarding the (4 when flat, 8 when curved) advice, I went with 8 all the time because as soon as your rotate / tilt a little, the bounding box have 8 corners. At the moment, the only things left are
21-03-05-12-32-28.mp4Current demo leave the overlaying html visible to ease the test. What do you think about the mobile support for selection + copy ? edit: The current selection issue on android chrome exist in the current troika example on master. |
This statement confused me so I looked at your current implementation. It looks like you're transforming the bbox to a world space bounding box, but that's going to fail as soon as your camera is in a non-default position. What you want to do is to do a full projection to screen coordinates. (geomBboxCorner * worldMatrix * cameraInverseMatrix * cameraProjectionMatrix) Since the geometry's bbox has only 4 corners when non-curved, that'll result in only 4 projected screen coordinates. Hope I'm making sense here. I'm tempted to take a swing at exact matching via CSS 3D transforms, the result would be much better at any non-orthagonal angle and it may not be any more complex than what you're having to do... 🤔 I'll try that out this weekend if you don't mind.
This feels like some odd browser quirk. I'll take a look. I'm not sure if you're feeling ready yet to try this out in another framework, but that will undoubtedly shake out some more issues and it could tell you, for example, if this is a bug in the Troika framework code or elsewhere.
🤷♂️ When you get to the code cleanup phase, I'd like to see if the logic for the DOM stuff and for displaying the selection rects can be extracted to separate modules that get instantiated as needed. (If they're treeshakeable too that would be ideal, but for that you'd have to create separate entry points (e.g. Text vs. AccessibleText) which doesn't feel great.) Thanks! |
While I want to agree with this because when it's non curved it's a plane, the three.js bbox helper is showing a square as soon as it start rotating. But maybe I'm missing something.
Sure ! Sounds difficult to me but maybe I'm missing something. I'm all for anything that could improve this.
I'm ready, I have to find how to point to this branch etc but shouldn't be a problem.
👍 Going with that
I agree that it would be best, I'll try that. |
Ahh, I see why that's confusing. Three's BoxHelper actually visualizes the world-axis-aligned bounding box (sometimes referred to as "aabb"), whereas we're talking about the geometry's local bounding box projected directly to screen coordinates. There's no reason for aligning to the world axes for this. |
@AlaricBaraou It wouldn't let me push it to your fork, but I committed the code for using CSS matrix3d to align the two DOM elements here: 1c09528 -- you can cherry-pick that in if you like. The alignment is improved, but:
You'll notice the DOM elements are always 10px x 10px; that's so they have a consistent starting size for the matrix3d transform. It also prevents reflows every frame which is much better for performance. The 10px size is arbitrary, it could be any other size as long as the matrix3d code is adjusted to match. I've removed the line that forced position:relative on the domContainer; that was having unexpected effects. If it had a purpose maybe we can find another solution. Let me know if you have any questions about this code! |
@lojjic Looks way better thx !
Looks good enough for me for a first version.
No, it was necessary for my solution, not needed with yours. I've just re-added the styles that make the text fit inside the dom element. It's necessary for the outline of screen readers. without it it would show a gigantic outline while with it it has an outline that pretty much fit the visible text. Not the biggest issue but it's still better and with your solution I assume it has no perf impact. |
In my opinion you're totally right. I barely started refactoring in order to make it work the way you described but I feel like it makes sense as I progress. |
Sorry missed this one. I don't remember any other reasons. |
@lojjic sorry for delay I still have to turn the "TextHighlighter" to a subclass of THREE.Group if I understood your suggestion correctly. Also I'm not using any eventEmitter for the selection like you suggested. I'm sorry that I'm asking you to provide so much answer / details. I can do the required changes but I prefer to be sure I'm doing the things the way you expect in order to avoid more change later. |
Great! This is coming together. In this separated form it's easier for me to see what code relates to what feature.
Yeah the idea there is that a "Highlight" should be an Object3D you can add as a child and give it your own arbitrary range, not necessarily tied to the selection range. Highlighting "find in text" results is a possible use case: const searchResults = findInText(text, searchTerm)
searchResults.forEach(({start, end}) => {
const highlight = new TextHighlight()
highlight.color = 'yellow'
highlight.startIndex = start
highlight.endIndex = end
textInstance.add(highlight)
}
Hmm, I do still think an EventEmitter would be a more flexible pattern, however... Seeing how little value the We could provide a sample implementation that consumes mouse events on the canvas (pure three.js users could use that if they wanted): addDragSelectionBehavior(textInstance, canvas, camera) ...and maybe export some additional helper functions the frameworks could utilize, and document it of course. But makeSelectable would just be about presenting the selection, not modifying it. What do you think about that? |
Sorry last couple of days happened to be quite busy. I made a few changes but I'm starting to feel a bit lost between the current vision and what I initially came up with. But little by little we're getting there 😁
For the record, you're probably right and it's just my lack of experience with it that don't allow me to see it. I continued to reorganise and now "TextHighlighter" is a subclass of THREE.Group and I removed startCaret/moveCaret and some other functions that didn't add much value.
Now, makeSelectable functions will read the current selection ( highlighted text ) from TextHighlight so everything can be highlighted from the TextHighlighter which make it usable in all kind of environment.
Should this be a new example ?
So far I didn't add any export. Maybe I'll see what might be useful when making the sample pure three.js implementation |
Maybe let's hold off on this for now, it could be a followup after merge.
I'm sorry about that, I know I threw a ton of major changes at you and some of them were only partially thought out. I tend to work through difficult problems like this with these sorts of stream-of-consciousness brainstorms; that's usually good for finding robust solutions, but I know it can be frustrating if you're on the receiving end of it. I'll say, though, that you've done a wonderful job of making sense of my ramblings!! 😄 So, how are you feeling about the current state? Do you have a to-do list left, or is it where you'd like it to be? I'd like to get this finished off and merged/released soon, and I'm willing to take over at any point for the finishing touches, but I won't step on your toes if you're still eager to keep working on it. |
I would love it too if it could be finished soon :) Take over whenever you want, it will be easier that way. |
Starting now, I'll update this as I progress
Text Selection
Text accessibility
|
@lojjic Everything is fine for me feature wide. Let me know if you need some infos on some code or anything. |
Great! Thanks for the checklist, that will be helpful for future testing too. Would you mind resolving the conflict before I start pulling this in? |
@lojjic Sorry it looks like there was one conflict remaining.. It's resolved now, sorry for the delay. |
Don't worry about any more conflicts, I've been making other changes that overlap a bit. I can resolve them when I get back to this (soon). |
This PR looks awesome, and reading through your collaboration comments is kind of an incredible tribute to you both as human beings. Well done. |
A quick ping - last activity was 1 year ago - is there a future for this very useful feature? |
Personally I don't have enough time to dive into Troika in the near future and couldn't commit to maintain the feature. |
Hey @lojjic |
This is a work in progress, I've reached the point where I'll need feedbacks and advice in order to do a clean first version of this.
I'll list all the features below and the currents bugs for which I need guidance.
Screen readers support :
I added a property domContainer to chose where to append the dom if specified.
haven't tested this property yet but it will be useful with library like react-three-a11y that allow to structure your HTML.
It's not enabled by default because syncing html position can slow down FPS if a lot of Text instanced are used. Also, not all Text used in a canvas need to be read. So, to have this feature off by default seems to be the good option IMO.
Browser translation support :
How to test : Right click the page ( outside of the canvas ) and click translate to X, then the rendered text should update
Selection support :
Selection custom color / material :
The property selectionMaterial allow to change the material of the selection box.
Copy Selection support :
How to test : Right click the selection then click 'Copy' or use shortcut like Ctrl + C
Current issues :
I'll update the above as I keep updating the PR
I have a lot of time to give to this PR but some advice on best practices, ideas etc are more than welcome.
You can test everything by running the examples, if you just want to take a quick look, it should be available here https://deploy-preview-110--troika-examples.netlify.app/#text
regarding code quality / comments, I'll do that at the end, once the features works the way we decided to make them work.