Porównaj commity

...

45 Commity

Autor SHA1 Wiadomość Data
Steve Ruiz 64e4f8e16d Update editor.mdx 2024-04-22 17:51:31 +01:00
Steve Ruiz cb99199f24 Delete camera.mdx 2024-04-22 17:14:48 +01:00
Steve Ruiz 8565e0ad29 remove rendering bounds 2024-04-22 17:14:27 +01:00
Steve Ruiz 0e50cd4f36 move camera state out, trim unused methods 2024-04-22 16:37:32 +01:00
Steve Ruiz 849f94851d Update camera.mdx 2024-04-22 16:18:13 +01:00
Steve Ruiz 10f94eafe9 more renaming, docs 2024-04-22 15:49:32 +01:00
Steve Ruiz 7230a90e59 Merge branch 'camera-controls-api' of https://github.com/tldraw/tldraw into camera-controls-api 2024-04-22 11:00:03 +01:00
Steve Ruiz 1d237b1dd6
Merge branch 'main' into camera-controls-api 2024-04-22 09:17:34 +01:00
Steve Ruiz 04218652b3 ok 2024-04-22 09:14:34 +01:00
Steve Ruiz e9b13ab9cf ok 2024-04-21 14:29:15 +01:00
Steve Ruiz 219e31ada3 update APIs 2024-04-21 14:26:06 +01:00
dependabot[bot] 4507ce6378
Bump the npm_and_yarn group across 1 directory with 2 updates (#3505)
Bumps the npm_and_yarn group with 2 updates in the / directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and
[tar](https://github.com/isaacs/node-tar).

Updates `vite` from 5.2.8 to 5.2.9
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.2.9 (2024-04-15)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: <code>fsp.rm</code> removing files does not take effect (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16032">#16032</a>)
(<a href="https://github.com/vitejs/vite/commit/b05c405">b05c405</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16032">#16032</a></li>
<li>fix: fix accumulated stacks in error overlay (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16393">#16393</a>)
(<a href="https://github.com/vitejs/vite/commit/102c2fd">102c2fd</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16393">#16393</a></li>
<li>fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16376">#16376</a>)
(<a href="https://github.com/vitejs/vite/commit/58a2938">58a2938</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16376">#16376</a></li>
<li>chore: update region comment (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16380">#16380</a>)
(<a href="https://github.com/vitejs/vite/commit/77562c3">77562c3</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16380">#16380</a></li>
<li>perf: reduce size of injected __vite__mapDeps code (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16184">#16184</a>)
(<a href="https://github.com/vitejs/vite/commit/c0ec6be">c0ec6be</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16184">#16184</a></li>
<li>perf(css): only replace empty chunk if imported (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16349">#16349</a>)
(<a href="https://github.com/vitejs/vite/commit/e2658ad">e2658ad</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16349">#16349</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a77707d69c"><code>a77707d</code></a>
release: v5.2.9</li>
<li><a
href="102c2fd5ad"><code>102c2fd</code></a>
fix: fix accumulated stacks in error overlay (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16393">#16393</a>)</li>
<li><a
href="58a2938a97"><code>58a2938</code></a>
fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16376">#16376</a>)</li>
<li><a
href="77562c3ff2"><code>77562c3</code></a>
chore: update region comment (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16380">#16380</a>)</li>
<li><a
href="b05c405f68"><code>b05c405</code></a>
fix: <code>fsp.rm</code> removing files does not take effect (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16032">#16032</a>)</li>
<li><a
href="e2658ad6fe"><code>e2658ad</code></a>
perf(css): only replace empty chunk if imported (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16349">#16349</a>)</li>
<li><a
href="c0ec6bea69"><code>c0ec6be</code></a>
perf: reduce size of injected __vite__mapDeps code (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16184">#16184</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v5.2.9/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Updates `tar` from 6.2.1 to 7.0.1
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md">tar's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>7.0</h2>
<ul>
<li>Rewrite in TypeScript, provide ESM and CommonJS hybrid
interface</li>
<li>Add tree-shake friendly exports, like
<code>import('tar/create')</code>
and <code>import('tar/read-entry')</code> to get individual functions or
classes.</li>
<li>Add <code>chmod</code> option that defaults to false, and deprecate
<code>noChmod</code>. That is, reverse the default option regarding
explicitly setting file system modes to match tar entry
settings.</li>
<li>Add <code>processUmask</code> option to avoid having to call
<code>process.umask()</code> when <code>chmod: true</code> (or
<code>noChmod: false</code>) is
set.</li>
</ul>
<h2>6.2</h2>
<ul>
<li>Add support for brotli compression</li>
<li>Add <code>maxDepth</code> option to prevent extraction into
excessively
deep folders.</li>
</ul>
<h2>6.1</h2>
<ul>
<li>remove dead link to benchmarks (<a
href="https://redirect.github.com/isaacs/node-tar/issues/313">#313</a>)
(<a href="https://github.com/yetzt"><code>@​yetzt</code></a>)</li>
<li>add examples/explanation of using tar.t (<a
href="https://github.com/isaacs"><code>@​isaacs</code></a>)</li>
<li>ensure close event is emited after stream has ended (<a
href="https://github.com/webark"><code>@​webark</code></a>)</li>
<li>replace deprecated String.prototype.substr() (<a
href="https://github.com/CommanderRoot"><code>@​CommanderRoot</code></a>,
<a
href="https://github.com/lukekarrys"><code>@​lukekarrys</code></a>)</li>
</ul>
<h2>6.0</h2>
<ul>
<li>Drop support for node 6 and 8</li>
<li>fix symlinks and hardlinks on windows being packed with
<code>\</code>-style path targets</li>
</ul>
<h2>5.0</h2>
<ul>
<li>Address unpack race conditions using path reservations</li>
<li>Change large-numbers errors from TypeError to Error</li>
<li>Add <code>TAR_*</code> error codes</li>
<li>Raise <code>TAR_BAD_ARCHIVE</code> warning/error when there are no
valid
entries found in an archive</li>
<li>do not treat ignored entries as an invalid archive</li>
<li>drop support for node v4</li>
<li>unpack: conditionally use a file mapping to write files on
Windows</li>
<li>Set more portable 'mode' value in portable mode</li>
<li>Set <code>portable</code> gzip option in portable mode</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d99fce38eb"><code>d99fce3</code></a>
7.0.1</li>
<li><a
href="af043922c0"><code>af04392</code></a>
Do not apply linkpath,global from global pax header</li>
<li><a
href="b0fbdea463"><code>b0fbdea</code></a>
7.0.0</li>
<li><a
href="957da7506c"><code>957da75</code></a>
remove old lib folder</li>
<li><a
href="9a260c2dba"><code>9a260c2</code></a>
test verifying <a
href="https://redirect.github.com/isaacs/node-tar/issues/398">#398</a>
is fixed</li>
<li><a
href="2d89a4edc3"><code>2d89a4e</code></a>
Properly handle long linkpath in PaxHeader</li>
<li><a
href="314ec7e642"><code>314ec7e</code></a>
list: close file even if no error thrown</li>
<li><a
href="b3afdbb264"><code>b3afdbb</code></a>
unpack test: use modern tap features</li>
<li><a
href="2330416081"><code>2330416</code></a>
test: code style, prefer () to _ for empty fns</li>
<li><a
href="ae9ce7ec2a"><code>ae9ce7e</code></a>
test: fix normalize-unicode coverage on linux</li>
<li>Additional commits viewable in <a
href="https://github.com/isaacs/node-tar/compare/v6.2.1...v7.0.1">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/tldraw/tldraw/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mime Čuvalo <mimecuvalo@gmail.com>
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-21 12:39:38 +00:00
Steve Ruiz a6f3241c8f Merge branch 'main' into camera-controls-api 2024-04-21 13:39:02 +01:00
Steve Ruiz a6d2ab05d2
Perf: minor drawing speedup (#3464)
Tiny changes as I walk through freehand code. These would only really
make a difference on pages with many freehand shapes.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features

### Release Notes

- Improve performance of draw shapes.
2024-04-21 11:46:35 +00:00
Steve Ruiz b5fab15c6d
Prevent default on native clipboard events (#3536)
This PR calls prevent default on native clipboard events. This prevents
the error sound on Safari.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `bugfix` — Bug fix

### Test Plan

1. Use the cut, copy, and paste events on Safari.
2. Everything should still work, but no sounds should play.

### Release Notes

- Fix copy sound on clipboard events.
2024-04-21 11:45:55 +00:00
David Sheldrick b5dfd81540
WebGL Minimap (#3510)
This PR replaces our current minimap implementation with one that uses
WebGL

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know


### Test Plan

1. Add a step-by-step description of how to test your PR here.
2.

- [ ] Unit Tests
- [ ] End to end tests

### Release Notes

- Add a brief release note for your PR here.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2024-04-19 13:56:55 +00:00
Steve Ruiz f6a2e352de
Improve back to content (#3532)
This PR improves the "back to content" behavior. Rather than using an
interval, we now add a "camera-stopped" event that triggers the check.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` 

### Test Plan

1. Create some shapes, then move the camera to an empty part of the
canvas.
2. Check that the back to content button appears.
3. Ensure that the back to content button does not appear when the
canvas is empty.
2024-04-19 12:07:33 +00:00
Steve Ruiz b356cb1165 Update camera.mdx 2024-04-19 09:32:12 +01:00
Steve Ruiz afeaae83d5 Update camera.mdx 2024-04-18 17:53:36 +01:00
Steve Ruiz e929bdd133 fix up 2024-04-18 17:38:32 +01:00
Steve Ruiz caf9ec4ee1 rename initial to reset 2024-04-18 17:18:39 +01:00
Steve Ruiz cefe694798 Update camera.mdx 2024-04-18 16:28:27 +01:00
Steve Ruiz 4a5271ac3d Update camera.mdx 2024-04-18 15:35:37 +01:00
Steve Ruiz d1b270b4a3 fix 2024-04-18 15:28:48 +01:00
Steve Ruiz 3389687581 Update actions.tsx 2024-04-18 14:39:35 +01:00
Mitja Bezenšek 1fc68975e2
Fix version (#3521)
We were using react's version instead of the version of our packages.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [ ] `sdk` — Changes the tldraw SDK
- [x] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-18 13:38:57 +00:00
Steve Ruiz 009ecc2b5a ok 2024-04-18 14:33:42 +01:00
Steve Ruiz e209256084 ok 2024-04-18 14:23:28 +01:00
Steve Ruiz 6d03c73443 Update Editor.ts 2024-04-18 14:21:44 +01:00
Steve Ruiz 8aca34fd9a ok 2024-04-18 14:08:21 +01:00
Steve Ruiz ced1eadd8c ok 2024-04-18 14:05:39 +01:00
Steve Ruiz 20cbb6d4bf ok 2024-04-18 12:41:25 +01:00
Steve Ruiz a9d1c921c7 cleanup some pointer stuff 2024-04-18 11:37:54 +01:00
Mitja Bezenšek 47070ec109
Use computed cache for getting the parent child relationships (#3508)
Use the existing computed cache for parent child relationships instead
of creating it.

Tiny bit faster, less memory, and simpler.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [ ] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [x] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-18 08:01:46 +00:00
David Sheldrick 741ed00bda
[signia] Smart dirty checking of active computeds (#3516)
This is a huge perf win, and it came to me while procrastinating on
making dinner.

The idea is that we can skip checking the parents of a computed value if

- it is being dereferenced during a reaction cycle
- the computed value was not traversed during the current reaction cycle

This more than doubles the speed of the webgl minimap render on my
machine (from 2ms down to like 0.8ms).

This will make the biggest difference for anything that derives a value
from a large collection of other computed values where typically only a
small amount of them change at one time (e.g. iterating over all the
shape page bounds to compile an RBush)

Most code paths where we see a big chunk of `haveParentsChanged` in
flame graphs should be much faster after this.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
2024-04-18 07:57:37 +00:00
Mitja Bezenšek dd0b7b882d
VS Code 2.0.30 (#3519)
Version bump for the hotfix.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [ ] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [x] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-17 20:16:40 +00:00
David Sheldrick 625f4abc3b
[fix] allow loading files (#3517)
I messed up the schema validator for loading files.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-17 19:38:31 +00:00
Mitja Bezenšek f70fd2729d
VS Code 2.0.29 (#3515)
Version bump.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [ ] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [x] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [x] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-17 15:31:40 +00:00
Mime Čuvalo d247b5dc53
arrows: fix bound arrow labels going over text shape (#3512)
Fixes https://github.com/tldraw/tldraw/issues/3433

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know


### Release Notes

- Arrows: fix label positioning when bound.
2024-04-17 14:35:25 +00:00
Mime Čuvalo f9bafb2f8a
textfields: fix Safari cursor rendering bug, take 2 (#3513)
Take 2 on what this PR was trying to do:
https://github.com/tldraw/tldraw/pull/3373
Fixes https://github.com/tldraw/tldraw/issues/3398 hopefully this time
without the infinite recursion 🙃

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-17 14:31:35 +00:00
Mime Čuvalo f754bebc32
geo: fix double unique id on DOM (#3514)
Minor thing, but there's two nodes with the same ID. I got rid of the
one on the HTMLContainer, either one seems fine to remove though
¯\\_(ツ)_/¯

<img width="546" alt="Screenshot 2024-04-17 at 14 53 32"
src="https://github.com/tldraw/tldraw/assets/469604/5c4acdef-842c-4c4a-b9fd-504e23837efe">


### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-17 14:01:12 +00:00
Mime Čuvalo f44ea90da6
arrows: still use Dist instead of Dist2 (#3511)
A little regression from https://github.com/tldraw/tldraw/pull/3454. We
still need the exact distance here.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know

### Release Notes

- Fix arrow label positioning
2024-04-17 13:16:22 +00:00
Mitja Bezenšek 0b44a8b47a
Fix culling. (#3504)
Fixes culling for cases when another user would drag shapes inside your
viewport. We weren't correctly calculating the culling status for arrows
that might be bound to those shapes and also for shapes within dragged
in groups / frames.


### Change Type

<!--  Please select a 'Scope' label ️ -->

- [ ] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [x] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [x] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know


### Test Plan

1. Open the same room in two browsers / tabs.
2. Have some shapes that are visible in one browser, but not the other.
3. Drag these shapes so that they are visible in the other browser as
well.
4. They should correctly get unculled.
5. Do this by dragging shapes that have arrows bound to them (arrows
should uncull), groups (shapes within them should uncull), frames.

- [x] Unit Tests
- [ ] End to end tests

### Release Notes

- Fix culling.
2024-04-17 11:39:09 +00:00
Mime Čuvalo 34ad856873
textfields: nix disableTab option; make TextShapes have custom Tab behavior as intended (#3506)
We shouldn't be making this something you have to negate everytime you
use `useEditableText`. The TextShape can just have its custom behavior
since that's the intended usecase. (although I think that Tab there
doesn't do much anyway, but whatevs)

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [x] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-17 11:11:08 +00:00
Steve Ruiz 1450454873
"Soft preload" icons (#3507)
This PR includes a "soft preload" feature for icons, where icons will be
loaded when the canvas first mounts. The component will not wait for
icons to finish loading before showing the editor, but this should help
with "pop in" on menu icons.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features

### Test Plan

1. Load the component
2. After load, open a menu for the first time
3. The icons should immediately be visible

### Release Notes

- Improve icon preloading
2024-04-17 10:57:08 +00:00
80 zmienionych plików z 4664 dodań i 2094 usunięć

Wyświetl plik

@ -19,7 +19,7 @@ body:
id: reproduction
attributes:
label: How can we reproduce the bug?
description: If you can make the bug happen again, please share the steps involved.
description: If you can make the bug happen again, please share the steps involved. You can [fork this CodeSandbox](https://codesandbox.io/p/sandbox/tldraw-example-n539u) to make a reproduction.
validations:
required: false
- type: dropdown

Wyświetl plik

@ -201,7 +201,152 @@ The [Editor#getInstanceState](?) method returns settings that relate to each ind
The editor's user preferences are shared between all instances. See the [TLUserPreferences](?) docs for more about the user preferences.
## Common things to do with the editor
# Camera and coordinates
The editor offers many methods and properties relating to the part of the infinite canvas that is displayed in the component. This section includes key concepts and methods that you can use to change or control which parts of the canvas are visible.
## Viewport
The viewport is the rectangular area contained by the editor.
| Method | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [Editor#getViewportScreenBounds] | A [Box](?) that describes the size and position of the component's canvas in actual screen pixels. |
| [Editor#getViewportPageBounds](?) | A [Box](?) that describes the size and position of the part of the current page that is displayed in the viewport. |
## Screen vs. page coordinates
In tldraw, coordinates are either be in page or screen space.
A "screen point" refers to the point's distance from the top left corner of the component.
A "page point" refers to the point's distance from the "zero point" of the canvas.
When the camera is at `{x: 0, y: 0, z: 0}`, the screen point and page point will be identical. As the camera moves, however, the viewport will display a different part of the page; and so a screen point will correspond to a different page point.
| Method | Description |
| ------------------------ | ---------------------------------------------- |
| [Editor#screenToPage](?) | Convert a point in screen space to page space. |
| [Editor#pageToScreen](?) | Convert a point in page space to screen space. |
You can get the user's pointer position in both screen and page space.
```ts
const {
// The user's most recent page / screen points
currentPagePoint,
currentScreenPoint,
// The user's previous page / screen points
previousPagePoint,
previousScreenPoint,
// The last place where the most recent pointer down occurred
originPagePoint,
originScreenPoint,
} = editor.inputs
```
## Camera options
You can use the editor's camera options to configure the behavior of the editor's camera. There are many options available.
### `wheelBehavior`
When set to `'pan'`, scrolling the mousewheel will pan the camera. When set to `'zoom'`, scrolling the mousewheel will zoom the camera. When set to `none`, it will have no effect.
### `panSpeed`
The speed at which the camera pans. A pan can occur when the user holds the spacebar and drags, holds the middle mouse button and drags, drags while using the hand tool, or scrolls the mousewheel. The default value is `1`. A value of `0.5` would be twice as slow as default. A value of `2` would be twice as fast. When set to `0`, the camera will not pan.
### `zoomSpeed`
The speed at which the camera zooms. A zoom can occur when the user pinches or scrolls the mouse wheel. The default value is `1`. A value of `0.5` would be twice as slow as default. A value of `2` would be twice as fast. When set to `0`, the camera will not zoom.
### `zoomSteps`
The camera's "zoom steps" are an array of discrete zoom levels that the camera will move between when using the "zoom in" or "zoom out" controls.
The first number in the `zoomSteps` array defines the camera's minimum zoom level. The last number in the `zoomSteps` array defines the camera's maximum zoom level.
If the `constraints` are provided, then the actual value for the camera's zoom will be be calculated by multiplying the value from the `zoomSteps` array with the value from the `baseZoom`. See the `baseZoom` property for more information.
### `isLocked`
Whether the camera is locked. When the camera is locked, the camera will not move.
### `constraints`
By default the camera is free to move anywhere on the infinite canvas. However, you may provide the camera with a `constraints` object that constrains the camera based on a relationship between a `bounds` (in page space) and the viewport (in screen space).
### `constraints.bounds`
A box model describing the bounds in page space.
### `constraints.padding`
An object with padding to apply to the `x` and `y` dimensions of the viewport. The padding is in screen space.
### `constraints.origin`
An object with an origin for the `x` and `y` dimensions. Depending on the `behavior`, the origin may be used to position the bounds within the viewport.
For example, when the `behavior` is `fixed` and the `origin.x` is `0`, the bounds will be placed with its left side touching the left side of the viewport. When `origin.x` is `1` the bounds will be placed with its right side touching the right side of the viewport. By default the origin for each dimension is .5. This places the bounds in the center of the viewport.
### `constraints.initialZoom`
The `initialZoom` option defines the camera's initial zoom level and what the zoom should be when when the camera is reset. The zoom it produces is based on the value provided:
| Value | Description |
| --------- | ----------------------------------------------------------------------------------------------------------------------- |
| 'default' | 100%. |
| 'fit-x' | The zoom at which the constraint's `bounds.width` exactly fits within the viewport. |
| 'fit-y' | The zoom at which the constraint's `bounds.height` exactly fits within the viewport. |
| 'fit-min' | The zoom at which the _smaller_ of the constraint's `bounds.width` or `bounds.height` exactly fits within the viewport. |
| 'fit-max' | The zoom at which the _larger_ of the constraint's `bounds.width` or `bounds.height` exactly fits within the viewport. |
### `constraints.baseZoom`
The `baseZoom` property defines the base property for the camera's zoom steps. It accepts the same values as `initialZoom`.
When `constraints` are provided, then the actual value for the camera's zoom will be be calculated by multiplying the value from the `zoomSteps` array with the value from the `baseZoom`.
For example, if the `baseZoom` is set to `default`, then a zoom step of 2 will be 200%. However, if the `baseZoom` is set to `fit-x`, then a zoom step value of 2 will be twice the zoom level at which the bounds width exactly fits within the viewport.
### `constraints.behavior`
The `behavior` property defines which logic should be used when calculating the bounds position.
| Value | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 'free' | The bounds may be placed anywhere relative to the viewport. This is the default "infinite canvas" experience. |
| 'inside' | The bounds must stay entirely within the viewport. |
| 'outside' | The bounds may partially leave the viewport but must never leave it completely. |
| 'fixed' | The bounds are placed in the viewport at a fixed location according to the `'origin'`. |
| 'contain' | When the zoom is below the "fit zoom" for an axis, the bounds use the `'fixed'` behavior; when above, the bounds use the `inside` behavior. |
## Controlling the camera
There are several `Editor` methods available for controlling the camera.
| Method | Description |
| ------------------------------- | --------------------------------------------------------------------------------------------------- |
| [Editor#setCamera](?) | Moves the camera to the provided coordinates. |
| [Editor#zoomIn](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
| [Editor#zoomOut](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
| [Editor#zoomToFit](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
| [Editor#zoomToBounds](?) | Moves the camera to fit the given bounding box. |
| [Editor#zoomToSelection](?) | Moves the camera to fit the current selection. |
| [Editor#zoomToUser](?) | Moves the camera to center on a user's cursor. |
| [Editor#resetZoom](?) | Resets the zoom to 100% or to the `initialZoom` zoom level. |
| [Editor#centerOnPoint](?) | Centers the camera on the given point. |
| [Editor#stopCameraAnimation](?) | Stops any camera animation. |
## Camera state
The camera may be in two states, `idle` or `moving`.
You can get the current camera state with [Editor#getCameraState](?).
# Common things to do with the editor
### Create a shape id

Wyświetl plik

@ -1,5 +1,6 @@
import { ReactNode, useEffect, useState, version } from 'react'
import { ReactNode, useEffect, useState } from 'react'
import { LoadingScreen } from 'tldraw'
import { version } from '../../version'
import { useUrl } from '../hooks/useUrl'
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
@ -113,7 +114,7 @@ export function IFrameProtector({
<div className="tldraw__editor tl-container">
<div className="iframe-warning__container">
<a className="iframe-warning__link" href={url} target="_blank">
{'Visit this page on tldraw.com '}
{'Visit this page on tldraw.com'}
<svg
width="15"
height="15"

Wyświetl plik

@ -1,5 +1,5 @@
import { default as React, useEffect } from 'react'
import { Editor, TLPageId, clamp, debounce, react, useEditor } from 'tldraw'
import { Editor, TLPageId, Vec, clamp, debounce, react, useEditor } from 'tldraw'
const PARAMS = {
// deprecated
@ -68,19 +68,13 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) {
const viewport = viewportFromString(newViewportRaw)
const { x, y, w, h } = viewport
const { w: sw, h: sh } = editor.getViewportScreenBounds()
const fitZoom = editor.getCameraFitZoom()
const initialZoom = editor.getInitialZoom()
const { zoomSteps } = editor.getCameraOptions()
const zoomMin = zoomSteps[0]
const zoomMax = zoomSteps[zoomSteps.length - 1]
const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * fitZoom, zoomMax * fitZoom)
const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * initialZoom, zoomMax * initialZoom)
editor.setCamera(
{
x: -x + (sw - w * zoom) / 2 / zoom,
y: -y + (sh - h * zoom) / 2 / zoom,
z: zoom,
},
new Vec(-x + (sw - w * zoom) / 2 / zoom, -y + (sh - h * zoom) / 2 / zoom, zoom),
{ immediate: true }
)
} catch (err) {

Wyświetl plik

@ -86,7 +86,6 @@ export function useFileSystem({ isMultiplayer }: { isMultiplayer: boolean }): TL
editor.history.clear()
// Put the old bounds back in place
editor.updateViewportScreenBounds(bounds)
editor.updateRenderingBounds()
editor.updateInstanceState({ isFocused })
})
},

Wyświetl plik

@ -68,5 +68,5 @@ function createDemoShapes(editor: Editor) {
},
}))
)
.zoomToContent({ animation: { duration: 0 } })
.zoomToFit({ animation: { duration: 0 } })
}

Wyświetl plik

@ -58,5 +58,5 @@ function createDemoShapes(editor: Editor) {
})),
])
editor.zoomToContent({ animation: { duration: 0 } })
editor.zoomToFit({ animation: { duration: 0 } })
}

Wyświetl plik

@ -44,5 +44,5 @@ function createDemoShapes(editor: Editor) {
},
},
])
.zoomToContent({ animation: { duration: 0 } })
.zoomToFit({ animation: { duration: 0 } })
}

Wyświetl plik

@ -13,19 +13,20 @@ import 'tldraw/tldraw.css'
const CAMERA_OPTIONS: TLCameraOptions = {
isLocked: false,
wheelBehavior: 'pan',
panSpeed: 1,
zoomSpeed: 1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
constraints: {
fit: 'max',
initialZoom: 'fit-max',
baseZoom: 'fit-max',
bounds: {
x: 0,
y: 0,
w: 1200,
h: 800,
w: 1600,
h: 900,
},
fitX: 'contain',
fitY: 'contain',
behavior: { x: 'contain', y: 'contain' },
padding: { x: 100, y: 100 },
origin: { x: 0.5, y: 0.5 },
},
@ -139,11 +140,16 @@ const components = {
const CameraOptionsControlPanel = track(() => {
const editor = useEditor()
const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex', CAMERA_OPTIONS)
const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex1', CAMERA_OPTIONS)
useEffect(() => {
if (!editor) return
editor.setCameraOptions(cameraOptions, { immediate: true })
editor.batch(() => {
editor.setCameraOptions(cameraOptions, { immediate: true })
editor.setCamera(editor.getCamera(), {
immediate: true,
})
})
}, [editor, cameraOptions])
const { constraints } = cameraOptions
@ -234,10 +240,12 @@ const CameraOptionsControlPanel = track(() => {
<input
name="zoomsteps"
type="text"
value={cameraOptions.zoomSteps.join(', ')}
defaultValue={cameraOptions.zoomSteps.join(', ')}
onChange={(e) => {
const val = e.target.value.split(', ').map((v) => Number(v))
updateOptions({ zoomSteps: val })
if (val.every((v) => typeof v === 'number' && Number.isFinite(v))) {
updateOptions({ zoomSteps: val })
}
}}
/>
<label htmlFor="bounds">Bounds</label>
@ -275,24 +283,43 @@ const CameraOptionsControlPanel = track(() => {
</select>
{constraints ? (
<>
<label htmlFor="fit">Fit</label>
<label htmlFor="initialZoom">Initial Zoom</label>
<select
name="fit"
value={constraints.fit}
name="initialZoom"
value={constraints.initialZoom}
onChange={(e) => {
updateOptions({
constraints: {
...constraints,
fit: e.target.value as any,
initialZoom: e.target.value as any,
},
})
}}
>
<option>min</option>
<option>max</option>
<option> x</option>
<option> y</option>
<option>none</option>
<option>fit-min</option>
<option>fit-max</option>
<option>fit-x</option>
<option>fit-y</option>
<option>default</option>
</select>
<label htmlFor="zoomBehavior">Base Zoom</label>
<select
name="zoomBehavior"
value={constraints.baseZoom}
onChange={(e) => {
updateOptions({
constraints: {
...constraints,
baseZoom: e.target.value as any,
},
})
}}
>
<option>fit-min</option>
<option>fit-max</option>
<option>fit-x</option>
<option>fit-y</option>
<option>default</option>
</select>
<label htmlFor="originX">Origin X</label>
<input
@ -368,16 +395,19 @@ const CameraOptionsControlPanel = track(() => {
})
}}
/>
<label htmlFor="fitx">Fit X</label>
<label htmlFor="behaviorX">Behavior X</label>
<select
name="fitx"
value={constraints.fitX}
name="behaviorX"
value={(constraints.behavior as { x: any; y: any }).x}
onChange={(e) => {
setCameraOptions({
...cameraOptions,
constraints: {
...constraints,
fitX: e.target.value as any,
behavior: {
...(constraints.behavior as { x: any; y: any }),
x: e.target.value as any,
},
},
})
}}
@ -387,16 +417,19 @@ const CameraOptionsControlPanel = track(() => {
<option>outside</option>
<option>lock</option>
</select>
<label htmlFor="fity">Fit Y</label>
<label htmlFor="behaviorY">Behavior Y</label>
<select
name="fity"
value={constraints.fitY}
name="behaviorY"
value={(constraints.behavior as { x: any; y: any }).y}
onChange={(e) => {
setCameraOptions({
...cameraOptions,
constraints: {
...constraints,
fitY: e.target.value as any,
behavior: {
...(constraints.behavior as { x: any; y: any }),
y: e.target.value as any,
},
},
})
}}
@ -412,7 +445,7 @@ const CameraOptionsControlPanel = track(() => {
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<button
onClick={() => {
editor.setCamera(editor.getCamera(), { initial: true })
editor.setCamera(editor.getCamera(), { reset: true })
}}
>
Reset Camera

Wyświetl plik

@ -51,7 +51,7 @@ export default function ExternalContentSourcesExample() {
const htmlSource = sources?.find((s) => s.type === 'text' && s.subtype === 'html')
if (htmlSource) {
const center = point ?? editor.getViewportPageCenter()
const center = point ?? editor.getViewportPageBounds().center
editor.createShape({
type: 'html',

Wyświetl plik

@ -133,19 +133,19 @@ export function ImageAnnotationEditor({
editor.setCameraOptions(
{
constraints: {
fit: 'max',
initialZoom: 'fit-max',
baseZoom: 'default',
bounds: { w: image.width, h: image.height, x: 0, y: 0 },
padding: { x: 32, y: 64 },
origin: { x: 0.5, y: 0.5 },
fitX: 'inside',
fitY: 'inside',
behavior: 'inside',
},
zoomSteps: [1, 2, 4, 8],
zoomSpeed: 1,
panSpeed: 1,
isLocked: false,
},
{ initial: isInitial }
{ reset: isInitial }
)
isInitial = false

Wyświetl plik

@ -1,9 +0,0 @@
---
title: Rendering shapes change
component: ./RenderingShapesChangeExample.tsx
category: basic
---
---
Do something when the rendering shapes change.

Wyświetl plik

@ -1,27 +0,0 @@
import { useCallback } from 'react'
import { TLShape, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import { useChangedShapesReactor } from './useRenderingShapesChange'
const components = {
InFrontOfTheCanvas: () => {
const onShapesChanged = useCallback((info: { culled: TLShape[]; restored: TLShape[] }) => {
// eslint-disable-next-line no-console
for (const shape of info.culled) console.log('culled: ' + shape.id)
// eslint-disable-next-line no-console
for (const shape of info.restored) console.log('restored: ' + shape.id)
}, [])
useChangedShapesReactor(onShapesChanged)
return null
},
}
export default function RenderingShapesChangeExample() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="example" components={components} />
</div>
)
}

Wyświetl plik

@ -1,50 +0,0 @@
import { useEffect, useRef } from 'react'
import { TLShape, react, useEditor } from 'tldraw'
export function useChangedShapesReactor(
cb: (info: { culled: TLShape[]; restored: TLShape[] }) => void
) {
const editor = useEditor()
const rPrevShapes = useRef({
renderingShapes: editor.getRenderingShapes(),
culledShapes: editor.getCulledShapes(),
})
useEffect(() => {
return react('when rendering shapes change', () => {
const after = {
culledShapes: editor.getCulledShapes(),
renderingShapes: editor.getRenderingShapes(),
}
const before = rPrevShapes.current
const culled: TLShape[] = []
const restored: TLShape[] = []
const beforeToVisit = new Set(before.renderingShapes)
for (const afterInfo of after.renderingShapes) {
const beforeInfo = before.renderingShapes.find((s) => s.id === afterInfo.id)
if (!beforeInfo) {
continue
} else {
const isAfterCulled = after.culledShapes.has(afterInfo.id)
const isBeforeCulled = before.culledShapes.has(beforeInfo.id)
if (isAfterCulled && !isBeforeCulled) {
culled.push(afterInfo.shape)
} else if (!isAfterCulled && isBeforeCulled) {
restored.push(afterInfo.shape)
}
beforeToVisit.delete(beforeInfo)
}
}
rPrevShapes.current = after
cb({
culled,
restored,
})
})
}, [cb, editor])
}

Wyświetl plik

@ -203,7 +203,6 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
text={text}
labelColor={theme[color].solid}
isSelected={isSelected}
disableTab
wrap
/>
</>

Wyświetl plik

@ -50,7 +50,6 @@ const ZOOM_EVENT = {
'reset-zoom': 'resetZoom',
'zoom-to-fit': 'zoomToFit',
'zoom-to-selection': 'zoomToSelection',
'zoom-to-content': 'zoomToContent',
}
export function getCodeSnippet(name: string, data: any) {
@ -136,15 +135,11 @@ if (updates.length > 0) {
} else if (name === 'fit-frame-to-content') {
codeSnippet = `fitFrameToContent(editor, editor.getOnlySelectedShape().id)`
} else if (name.startsWith('zoom-') || name === 'reset-zoom') {
if (name === 'zoom-to-content') {
codeSnippet = 'editor.zoomToContent()'
} else {
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
? 'editor.getViewportScreenCenter(), '
: ''
}{ duration: 320 })`
}
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
? 'editor.getViewportScreenCenter(), '
: ''
}{ duration: 320 })`
} else if (name.startsWith('toggle-')) {
if (name === 'toggle-lock') {
codeSnippet = `editor.toggleLock(editor.getSelectedShapeIds())`

Wyświetl plik

@ -1,3 +1,13 @@
## 2.0.30
- Fixes a bug that prevented opening some files.
## 2.0.29
- Improved note shapes.
- Color improvements for both light and dark mode.
- Bug fixes and performance improvements.
## 2.0.28
- Fix an issue with panning the canvas.

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "tldraw-vscode",
"description": "The tldraw extension for VS Code.",
"version": "2.0.28",
"version": "2.0.30",
"private": true,
"author": {
"name": "tldraw Inc.",

Wyświetl plik

@ -11,6 +11,10 @@ import { TextDecoder, TextEncoder } from 'util'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
Image.prototype.decode = async function () {
return true
}
function convertNumbersInObject(obj: any, roundToNearest: number) {
if (!obj) return obj
if (Array.isArray(obj)) {

Wyświetl plik

@ -459,6 +459,9 @@ export const DEFAULT_ANIMATION_OPTIONS: {
easing: (t: number) => number;
};
// @internal (undocumented)
export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions;
// @public (undocumented)
export function DefaultBackground(): JSX_2.Element;
@ -593,7 +596,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}>;
force: boolean;
immediate: boolean;
initial: boolean;
reset: boolean;
}>): this;
animateShapes(partials: (null | TLShapePartial | undefined)[], opts?: Partial<{
animation: Partial<{
@ -602,7 +605,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}>;
force: boolean;
immediate: boolean;
initial: boolean;
reset: boolean;
}>): this;
// @internal (undocumented)
annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: {
@ -689,12 +692,14 @@ export class Editor extends EventEmitter<TLEventMap> {
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
getBaseZoom(): number;
getCamera(): TLCamera;
getCameraFitZoom(): number;
getCameraOptions(): TLCameraOptions;
getCameraState(): "idle" | "moving";
getCanRedo(): boolean;
getCanUndo(): boolean;
getCollaborators(): TLInstancePresence[];
getCollaboratorsOnCurrentPage(): TLInstancePresence[];
getContainer: () => HTMLElement;
getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined;
// @internal
@ -706,6 +711,8 @@ export class Editor extends EventEmitter<TLEventMap> {
getCurrentPageId(): TLPageId;
getCurrentPageRenderingShapesSorted(): TLShape[];
getCurrentPageShapeIds(): Set<TLShapeId>;
// @internal (undocumented)
getCurrentPageShapeIdsSorted(): TLShapeId[];
getCurrentPageShapes(): TLShape[];
getCurrentPageShapesSorted(): TLShape[];
getCurrentPageState(): TLInstancePageState;
@ -725,6 +732,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getHoveredShape(): TLShape | undefined;
getHoveredShapeId(): null | TLShapeId;
getInitialMetaForShape(_shape: TLShape): JsonObject;
getInitialZoom(): number;
getInstanceState(): TLInstance;
getIsMenuOpen(): boolean;
getOnlySelectedShape(): null | TLShape;
@ -738,7 +746,6 @@ export class Editor extends EventEmitter<TLEventMap> {
getPath(): string;
getPointInParentSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
getRenderingBounds(): Box;
getRenderingShapes(): {
backgroundIndex: number;
id: TLShapeId;
@ -813,7 +820,6 @@ export class Editor extends EventEmitter<TLEventMap> {
util: ShapeUtil;
}[];
getViewportPageBounds(): Box;
getViewportPageCenter(): Vec;
getViewportScreenBounds(): Box;
getViewportScreenCenter(): Vec;
getZoomLevel(): number;
@ -860,8 +866,6 @@ export class Editor extends EventEmitter<TLEventMap> {
packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this;
pageToScreen(point: VecLike): Vec;
pageToViewport(point: VecLike): Vec;
pan(offset: VecLike, opts?: TLCameraMoveOptions): this;
panZoomIntoView(ids: TLShapeId[], opts?: TLCameraMoveOptions): this;
popFocusedGroupId(): this;
putContentOntoCurrentPage(content: TLContent, options?: {
point?: VecLike;
@ -878,7 +882,6 @@ export class Editor extends EventEmitter<TLEventMap> {
type: T;
} : TLExternalContent) => void) | null): this;
renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
renderingBoundsMargin: number;
reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this;
resetZoom(point?: Vec, opts?: TLCameraMoveOptions): this;
resizeShape(shape: TLShape | TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this;
@ -892,11 +895,7 @@ export class Editor extends EventEmitter<TLEventMap> {
sendBackward(shapes: TLShape[] | TLShapeId[]): this;
sendToBack(shapes: TLShape[] | TLShapeId[]): this;
setCamera(point: VecLike, opts?: TLCameraMoveOptions): this;
setCameraOptions(options: Partial<TLCameraOptions>, opts?: {
force?: boolean;
immediate?: boolean;
initial?: boolean;
}): this;
setCameraOptions(options: Partial<TLCameraOptions>, opts?: TLCameraMoveOptions): this;
setCroppingShape(shape: null | TLShape | TLShapeId): this;
setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this;
setCurrentTool(id: string, info?: {}): this;
@ -943,8 +942,6 @@ export class Editor extends EventEmitter<TLEventMap> {
updateDocumentSettings(settings: Partial<TLDocument>): this;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLCommandHistoryOptions): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: TLCommandHistoryOptions): this;
// @internal
updateRenderingBounds(): this;
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, historyOptions?: TLCommandHistoryOptions): this;
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], historyOptions?: TLCommandHistoryOptions): this;
updateViewportScreenBounds(screenBounds: Box, center?: boolean): this;
@ -956,10 +953,8 @@ export class Editor extends EventEmitter<TLEventMap> {
inset?: number;
targetZoom?: number;
} & TLCameraMoveOptions): this;
zoomToContent(opts?: TLCameraMoveOptions): this;
zoomToFit(opts?: TLCameraMoveOptions): this;
zoomToSelection(opts?: TLCameraMoveOptions): this;
zoomToShape(shapeId: TLShapeId, opts?: TLCameraMoveOptions): this;
zoomToUser(userId: string, opts?: TLCameraMoveOptions): this;
}
@ -1093,9 +1088,6 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap
// @public (undocumented)
export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string;
// @internal (undocumented)
export const getDefaultCameraOptions: (cameraOptions?: Partial<TLCameraOptions>) => TLCameraOptions;
// @public (undocumented)
export function getFreshUserPreferences(): TLUserPreferences;
@ -1997,23 +1989,27 @@ export type TLBrushProps = {
// @public (undocumented)
export type TLCameraMoveOptions = Partial<{
animation: Partial<{
duration: number;
easing: (t: number) => number;
duration: number;
}>;
force: boolean;
immediate: boolean;
initial: boolean;
reset: boolean;
}>;
// @public (undocumented)
export type TLCameraOptions = {
wheelBehavior: 'none' | 'pan' | 'zoom';
constraints?: {
fitX: 'contain' | 'inside' | 'lock' | 'outside';
fitY: 'contain' | 'inside' | 'lock' | 'outside';
behavior: 'contain' | 'fixed' | 'free' | 'inside' | 'outside' | {
x: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
y: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
};
bounds: BoxModel;
baseZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';
initialZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';
origin: VecLike;
padding: VecLike;
fit: 'max' | 'min' | 'none' | 'x' | 'y';
};
panSpeed: number;
zoomSpeed: number;

Wyświetl plik

@ -7554,7 +7554,7 @@
},
{
"kind": "Content",
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n }>"
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n reset: boolean;\n }>"
},
{
"kind": "Content",
@ -7641,7 +7641,7 @@
},
{
"kind": "Content",
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n }>"
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n reset: boolean;\n }>"
},
{
"kind": "Content",
@ -9838,6 +9838,37 @@
"isAbstract": false,
"name": "getAssets"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getBaseZoom:member(1)",
"docComment": "/**\n * Get the camera's base level for calculating actual zoom levels based on the zoom steps.\n *\n * @example\n * ```ts\n * editor.getBaseZoom()\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getBaseZoom(): "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getBaseZoom"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCamera:member(1)",
@ -9874,37 +9905,6 @@
"isAbstract": false,
"name": "getCamera"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCameraFitZoom:member(1)",
"docComment": "/**\n * Get the zoom level that would fit the camera to the current constraints.\n *\n * @example\n * ```ts\n * editor.getCameraFitZoom()\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getCameraFitZoom(): "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getCameraFitZoom"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)",
@ -10030,6 +10030,86 @@
"isAbstract": false,
"name": "getCanUndo"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCollaborators:member(1)",
"docComment": "/**\n * Returns a list of presence records for all peer collaborators. This will return the latest presence record for each connected user.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getCollaborators(): "
},
{
"kind": "Content",
"text": "import(\"@tldraw/tlschema\")."
},
{
"kind": "Reference",
"text": "TLInstancePresence",
"canonicalReference": "@tldraw/tlschema!TLInstancePresence:interface"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 4
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getCollaborators"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCollaboratorsOnCurrentPage:member(1)",
"docComment": "/**\n * Returns a list of presence records for all peer collaborators on the current page. This will return the latest presence record for each connected user.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getCollaboratorsOnCurrentPage(): "
},
{
"kind": "Content",
"text": "import(\"@tldraw/tlschema\")."
},
{
"kind": "Reference",
"text": "TLInstancePresence",
"canonicalReference": "@tldraw/tlschema!TLInstancePresence:interface"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 4
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getCollaboratorsOnCurrentPage"
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!Editor#getContainer:member",
@ -11169,6 +11249,37 @@
"isAbstract": false,
"name": "getInitialMetaForShape"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getInitialZoom:member(1)",
"docComment": "/**\n * Get the camera's initial or reset zoom level.\n *\n * @example\n * ```ts\n * editor.getInitialZoom()\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getInitialZoom(): "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getInitialZoom"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getInstanceState:member(1)",
@ -11809,38 +11920,6 @@
"isAbstract": false,
"name": "getPointInShapeSpace"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getRenderingBounds:member(1)",
"docComment": "/**\n * The current rendering bounds in the current page space, used for checking which shapes are \"on screen\".\n *\n * @example\n * ```ts\n * editor.getRenderingBounds()\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getRenderingBounds(): "
},
{
"kind": "Reference",
"text": "Box",
"canonicalReference": "@tldraw/editor!Box:class"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getRenderingBounds"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getRenderingShapes:member(1)",
@ -14146,38 +14225,6 @@
"isAbstract": false,
"name": "getViewportPageBounds"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getViewportPageCenter:member(1)",
"docComment": "/**\n * The center of the viewport in the current page space.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getViewportPageCenter(): "
},
{
"kind": "Reference",
"text": "Vec",
"canonicalReference": "@tldraw/editor!Vec:class"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getViewportPageCenter"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getViewportScreenBounds:member(1)",
@ -15589,142 +15636,6 @@
"isAbstract": false,
"name": "pageToViewport"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#pan:member(1)",
"docComment": "/**\n * Pan the camera.\n *\n * @param offset - The offset in the current page space.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.pan({ x: 100, y: 100 })\n * editor.pan({ x: 100, y: 100 }, { animation: { duration: 1000 } })\n * ```\n *\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "pan(offset: "
},
{
"kind": "Reference",
"text": "VecLike",
"canonicalReference": "@tldraw/editor!VecLike:type"
},
{
"kind": "Content",
"text": ", opts?: "
},
{
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "this"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "offset",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "pan"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#panZoomIntoView:member(1)",
"docComment": "/**\n * Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible.\n *\n * @param ids - The ids of the shapes to pan and zoom into view.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.panZoomIntoView([myShape.id])\n * editor.panZoomIntoView([myShape.id], { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "panZoomIntoView(ids: "
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ", opts?: "
},
{
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "this"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 6,
"endIndex": 7
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "ids",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isOptional": false
},
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 4,
"endIndex": 5
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "panZoomIntoView"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#popFocusedGroupId:member(1)",
@ -16240,36 +16151,6 @@
"isAbstract": false,
"name": "renamePage"
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!Editor#renderingBoundsMargin:member",
"docComment": "/**\n * The distance to expand the viewport when measuring culling. A larger distance will mean that shapes near to the viewport (but still outside of it) will not be culled.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "renderingBoundsMargin: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "renderingBoundsMargin",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#reparentShapes:member(1)",
@ -17060,8 +16941,9 @@
"text": ", opts?: "
},
{
"kind": "Content",
"text": "{\n force?: boolean;\n immediate?: boolean;\n initial?: boolean;\n }"
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
@ -19799,55 +19681,6 @@
"isAbstract": false,
"name": "zoomToBounds"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToContent:member(1)",
"docComment": "/**\n * Move the camera to the nearest content.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToContent()\n * editor.zoomToContent({ animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "zoomToContent(opts?: "
},
{
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "this"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "zoomToContent"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToFit:member(1)",
@ -19946,72 +19779,6 @@
"isAbstract": false,
"name": "zoomToSelection"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToShape:member(1)",
"docComment": "/**\n * Animate the camera to a shape.\n *\n * @param shapeId - The id of the shape to animate to.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToShape(myShape.id)\n * editor.zoomToShape(myShape.id, { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "zoomToShape(shapeId: "
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": ", opts?: "
},
{
"kind": "Reference",
"text": "TLCameraMoveOptions",
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "this"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "shapeId",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": true
}
],
"isOptional": false,
"isAbstract": false,
"name": "zoomToShape"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToUser:member(1)",
@ -36940,14 +36707,14 @@
},
{
"kind": "Content",
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n}>"
"text": "<{\n easing: (t: number) => number;\n duration: number;\n }>;\n force: boolean;\n immediate: boolean;\n reset: boolean;\n}>"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/editor/Editor.ts",
"fileUrlPath": "packages/editor/src/lib/editor/types/misc-types.ts",
"releaseTag": "Public",
"name": "TLCameraMoveOptions",
"typeTokenRange": {
@ -36966,7 +36733,7 @@
},
{
"kind": "Content",
"text": "{\n constraints?: {\n fitX: 'contain' | 'inside' | 'lock' | 'outside';\n fitY: 'contain' | 'inside' | 'lock' | 'outside';\n bounds: "
"text": "{\n wheelBehavior: 'none' | 'pan' | 'zoom';\n constraints?: {\n behavior: 'contain' | 'fixed' | 'free' | 'inside' | 'outside' | {\n x: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';\n y: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';\n };\n bounds: "
},
{
"kind": "Reference",
@ -36975,7 +36742,7 @@
},
{
"kind": "Content",
"text": ";\n origin: "
"text": ";\n baseZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';\n initialZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';\n origin: "
},
{
"kind": "Reference",
@ -36993,7 +36760,7 @@
},
{
"kind": "Content",
"text": ";\n fit: 'max' | 'min' | 'none' | 'x' | 'y';\n };\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n isLocked: boolean;\n}"
"text": ";\n };\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n isLocked: boolean;\n}"
},
{
"kind": "Content",

Wyświetl plik

@ -111,6 +111,7 @@ export {
ANIMATION_SHORT_MS,
CAMERA_SLIDE_FRICTION,
DEFAULT_ANIMATION_OPTIONS,
DEFAULT_CAMERA_OPTIONS,
DOUBLE_CLICK_DURATION,
DRAG_DISTANCE,
GRID_STEPS,
@ -120,14 +121,8 @@ export {
MULTI_CLICK_DURATION,
SIDES,
SVG_PADDING,
getDefaultCameraOptions,
} from './lib/constants'
export {
Editor,
type TLCameraMoveOptions,
type TLEditorOptions,
type TLResizeShapeOptions,
} from './lib/editor/Editor'
export { Editor, type TLEditorOptions, type TLResizeShapeOptions } from './lib/editor/Editor'
export type {
SideEffectManager,
TLAfterChangeHandler,
@ -240,6 +235,7 @@ export {
} from './lib/editor/types/history-types'
export {
type RequiredKeys,
type TLCameraMoveOptions,
type TLCameraOptions,
type TLSvgOptions,
} from './lib/editor/types/misc-types'

Wyświetl plik

@ -11,24 +11,15 @@ export const ANIMATION_SHORT_MS = 80
/** @internal */
export const ANIMATION_MEDIUM_MS = 320
const DEFAULT_COMMON_CAMERA_OPTIONS = {
zoomMax: 8,
zoomMin: 0.1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
zoomSpeed: 1,
panSpeed: 1,
/** @internal */
export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = {
isLocked: false,
wheelBehavior: 'pan',
panSpeed: 1,
zoomSpeed: 1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
}
/** @internal */
export const getDefaultCameraOptions = (
cameraOptions: Partial<TLCameraOptions> = {}
): TLCameraOptions => ({
...DEFAULT_COMMON_CAMERA_OPTIONS,
...cameraOptions,
})
/** @internal */
export const FOLLOW_CHASE_PROPORTION = 0.5
/** @internal */
export const FOLLOW_CHASE_PAN_SNAP = 0.1
@ -116,3 +107,8 @@ export const LONG_PRESS_DURATION = 500
/** @internal */
export const TEXT_SHADOW_LOD = 0.35
export const LEFT_MOUSE_BUTTON = 0
export const RIGHT_MOUSE_BUTTON = 2
export const MIDDLE_MOUSE_BUTTON = 1
export const STYLUS_ERASER_BUTTON = 5

Wyświetl plik

@ -1,5 +1,5 @@
import { RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
import { TLPageId, TLShapeId, isShape, isShapeId } from '@tldraw/tlschema'
import { computed, isUninitialized } from '@tldraw/state'
import { TLShapeId } from '@tldraw/tlschema'
import { Box } from '../../primitives/Box'
import { Editor } from '../Editor'
@ -20,16 +20,9 @@ function isShapeNotVisible(editor: Editor, id: TLShapeId, viewportPageBounds: Bo
* @returns Incremental derivation of non visible shapes.
*/
export const notVisibleShapes = (editor: Editor) => {
const isCullingOffScreenShapes = Number.isFinite(editor.renderingBoundsMargin)
const shapeHistory = editor.store.query.filterHistory('shape')
let lastPageId: TLPageId | null = null
let prevViewportPageBounds: Box
function fromScratch(editor: Editor): Set<TLShapeId> {
const shapes = editor.getCurrentPageShapeIds()
lastPageId = editor.getCurrentPageId()
const viewportPageBounds = editor.getViewportPageBounds()
prevViewportPageBounds = viewportPageBounds.clone()
const notVisibleShapes = new Set<TLShapeId>()
shapes.forEach((id) => {
if (isShapeNotVisible(editor, id, viewportPageBounds)) {
@ -38,68 +31,19 @@ export const notVisibleShapes = (editor: Editor) => {
})
return notVisibleShapes
}
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue, lastComputedEpoch) => {
if (!isCullingOffScreenShapes) return new Set<TLShapeId>()
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue) => {
if (isUninitialized(prevValue)) {
return fromScratch(editor)
}
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
if (diff === RESET_VALUE) {
return fromScratch(editor)
}
const nextValue = fromScratch(editor)
const currentPageId = editor.getCurrentPageId()
if (lastPageId !== currentPageId) {
return fromScratch(editor)
}
const viewportPageBounds = editor.getViewportPageBounds()
if (!prevViewportPageBounds || !viewportPageBounds.equals(prevViewportPageBounds)) {
return fromScratch(editor)
}
let nextValue = null as null | Set<TLShapeId>
const addId = (id: TLShapeId) => {
// Already added
if (prevValue.has(id)) return
if (!nextValue) nextValue = new Set(prevValue)
nextValue.add(id)
}
const deleteId = (id: TLShapeId) => {
// No need to delete since it's not there
if (!prevValue.has(id)) return
if (!nextValue) nextValue = new Set(prevValue)
nextValue.delete(id)
}
for (const changes of diff) {
for (const record of Object.values(changes.added)) {
if (isShape(record)) {
const isCulled = isShapeNotVisible(editor, record.id, viewportPageBounds)
if (isCulled) {
addId(record.id)
}
}
}
for (const [_from, to] of Object.values(changes.updated)) {
if (isShape(to)) {
const isCulled = isShapeNotVisible(editor, to.id, viewportPageBounds)
if (isCulled) {
addId(to.id)
} else {
deleteId(to.id)
}
}
}
for (const id of Object.keys(changes.removed)) {
if (isShapeId(id)) {
deleteId(id)
}
if (prevValue.size !== nextValue.size) return nextValue
for (const prev of prevValue) {
if (!nextValue.has(prev)) {
return nextValue
}
}
return nextValue ?? prevValue
return prevValue
})
}

Wyświetl plik

@ -95,116 +95,118 @@ export class ClickManager {
lastPointerInfo = {} as TLPointerEventInfo
/**
* Start the double click timeout.
*
* @param info - The event info.
*/
transformPointerDownEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
if (!this._clickState) return info
handlePointerEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
switch (info.name) {
case 'pointer_down': {
if (!this._clickState) return info
this._clickScreenPoint = Vec.From(info.point)
this._clickScreenPoint = Vec.From(info.point)
if (
this._previousScreenPoint &&
this._previousScreenPoint.dist(this._clickScreenPoint) > MAX_CLICK_DISTANCE
) {
this._clickState = 'idle'
}
this._previousScreenPoint = this._clickScreenPoint
this.lastPointerInfo = info
switch (this._clickState) {
case 'idle': {
this._clickState = 'pendingDouble'
this._clickTimeout = this._getClickTimeout(this._clickState)
return info // returns the pointer event
}
case 'pendingDouble': {
this._clickState = 'pendingTriple'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'double_click',
phase: 'down',
if (
this._previousScreenPoint &&
Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2
) {
this._clickState = 'idle'
}
}
case 'pendingTriple': {
this._clickState = 'pendingQuadruple'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'triple_click',
phase: 'down',
this._previousScreenPoint = this._clickScreenPoint
this.lastPointerInfo = info
switch (this._clickState) {
case 'pendingDouble': {
this._clickState = 'pendingTriple'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'double_click',
phase: 'down',
}
}
case 'pendingTriple': {
this._clickState = 'pendingQuadruple'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'triple_click',
phase: 'down',
}
}
case 'pendingQuadruple': {
this._clickState = 'pendingOverflow'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'quadruple_click',
phase: 'down',
}
}
case 'idle': {
this._clickState = 'pendingDouble'
break
}
case 'pendingOverflow': {
this._clickState = 'overflow'
break
}
default: {
// overflow
}
}
}
case 'pendingQuadruple': {
this._clickState = 'pendingOverflow'
this._clickTimeout = this._getClickTimeout(this._clickState)
return {
...info,
type: 'click',
name: 'quadruple_click',
phase: 'down',
}
}
case 'pendingOverflow': {
this._clickState = 'overflow'
this._clickTimeout = this._getClickTimeout(this._clickState)
return info
}
default: {
// overflow
this._clickTimeout = this._getClickTimeout(this._clickState)
return info
}
}
}
/**
* Emit click_up events on pointer up.
*
* @param info - The event info.
*/
transformPointerUpEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
if (!this._clickState) return info
this._clickScreenPoint = Vec.From(info.point)
switch (this._clickState) {
case 'pendingTriple': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'double_click',
phase: 'up',
}
}
case 'pendingQuadruple': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'triple_click',
phase: 'up',
}
}
case 'pendingOverflow': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'quadruple_click',
phase: 'up',
}
}
default: {
// idle, pendingDouble, overflow
case 'pointer_up': {
if (!this._clickState) return info
this._clickScreenPoint = Vec.From(info.point)
switch (this._clickState) {
case 'pendingTriple': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'double_click',
phase: 'up',
}
}
case 'pendingQuadruple': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'triple_click',
phase: 'up',
}
}
case 'pendingOverflow': {
return {
...this.lastPointerInfo,
type: 'click',
name: 'quadruple_click',
phase: 'up',
}
}
default: {
// idle, pendingDouble, overflow
}
}
return info
}
case 'pointer_move': {
if (
this._clickState !== 'idle' &&
this._clickScreenPoint &&
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
) {
this.cancelDoubleClickTimeout()
}
return info
}
}
return info
}
/**
@ -216,21 +218,4 @@ export class ClickManager {
this._clickTimeout = clearTimeout(this._clickTimeout)
this._clickState = 'idle'
}
/**
* Handle a move event, possibly cancelling the click timeout.
*
* @internal
*/
handleMove = () => {
// Cancel a double click event if the user has started dragging.
if (
this._clickState !== 'idle' &&
this._clickScreenPoint &&
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
) {
this.cancelDoubleClickTimeout()
}
}
}

Wyświetl plik

@ -64,7 +64,7 @@ export class SnapManager {
// TODO: make this an incremental derivation
@computed getSnappableShapes(): Set<TLShapeId> {
const { editor } = this
const renderingBounds = editor.getRenderingBounds()
const renderingBounds = editor.getViewportPageBounds()
const selectedShapeIds = editor.getSelectedShapeIds()
const snappableShapes: Set<TLShapeId> = new Set()

Wyświetl plik

@ -17,8 +17,27 @@ export type TLSvgOptions = {
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
}
/** @public */
export type TLCameraMoveOptions = Partial<{
/** Whether to move the camera immediately, rather than on the next tick. */
immediate: boolean
/** Whether to force the camera to move, even if the user's camera options have locked the camera. */
force: boolean
/** Whether to reset the camera to its default position and zoom. */
reset: boolean
/** An (optional) animation to use. */
animation: Partial<{
/** The time the animation should take to arrive at the specified camera coordinates. */
duration: number
/** An easing function to apply to the animation's progress from start to end. */
easing: (t: number) => number
}>
}>
/** @public */
export type TLCameraOptions = {
/** Controls whether the wheel pans or zooms. */
wheelBehavior: 'zoom' | 'pan' | 'none'
/** The speed of a scroll wheel / trackpad pan */
panSpeed: number
/** The speed of a scroll wheel / trackpad zoom */
@ -29,17 +48,26 @@ export type TLCameraOptions = {
isLocked: boolean
/** The camera constraints */
constraints?: {
/** The type of constraint behavior. */
fit: 'min' | 'max' | 'x' | 'y' | 'none'
/** The behavior for the constraints on the x axis. */
fitX: 'contain' | 'inside' | 'outside' | 'lock'
/** The behavior for the constraints on the y axis. */
fitY: 'contain' | 'inside' | 'outside' | 'lock'
/** The bounds of the content (in page space) */
/** The bounds (in page space) of the constrained space */
bounds: BoxModel
/** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */
/** The padding inside of the viewport (in screen space) */
padding: VecLike
/** The origin for placement when the bounds are smaller than the viewport. Provide a number for x and y, or [x, y].*/
/** The origin for placement. Used to position the bounds within the viewport when an axis is fixed or contained and zoom is below the axis fit. */
origin: VecLike
/** The camera's initial zoom, used also when the camera is reset. */
initialZoom: 'fit-min' | 'fit-max' | 'fit-x' | 'fit-y' | 'default'
/** The camera's base for its zoom steps. */
baseZoom: 'fit-min' | 'fit-max' | 'fit-x' | 'fit-y' | 'default'
/** The behavior for the constraints on the x axis. */
behavior:
| 'free'
| 'contain'
| 'inside'
| 'outside'
| 'fixed'
| {
x: 'contain' | 'inside' | 'outside' | 'fixed' | 'free'
y: 'contain' | 'inside' | 'outside' | 'fixed' | 'free'
}
}
}

Wyświetl plik

@ -1,4 +1,5 @@
import React, { useMemo } from 'react'
import { RIGHT_MOUSE_BUTTON } from '../constants'
import {
preventDefault,
releasePointerCapture,
@ -19,7 +20,7 @@ export function useCanvasEvents() {
function onPointerDown(e: React.PointerEvent) {
if ((e as any).isKilled) return
if (e.button === 2) {
if (e.button === RIGHT_MOUSE_BUTTON) {
editor.dispatch({
type: 'pointer',
target: 'canvas',

Wyświetl plik

@ -1,5 +1,4 @@
import { useComputed, useValue } from '@tldraw/state'
import { useMemo } from 'react'
import { uniq } from '../utils/uniq'
import { useEditor } from './useEditor'
@ -10,17 +9,12 @@ import { useEditor } from './useEditor'
*/
export function usePeerIds() {
const editor = useEditor()
const $presences = useMemo(() => {
return editor.store.query.records('instance_presence', () => ({
userId: { neq: editor.user.getId() },
}))
}, [editor])
const $userIds = useComputed(
'userIds',
() => uniq($presences.get().map((p) => p.userId)).sort(),
() => uniq(editor.getCollaborators().map((p) => p.userId)).sort(),
{ isEqual: (a, b) => a.join(',') === b.join?.(',') },
[$presences]
[editor]
)
return useValue($userIds)

Wyświetl plik

@ -1,6 +1,5 @@
import { useValue } from '@tldraw/state'
import { TLInstancePresence } from '@tldraw/tlschema'
import { useMemo } from 'react'
import { useEditor } from './useEditor'
// TODO: maybe move this to a computed property on the App class?
@ -11,21 +10,12 @@ import { useEditor } from './useEditor'
export function usePresence(userId: string): TLInstancePresence | null {
const editor = useEditor()
const $presences = useMemo(() => {
return editor.store.query.records('instance_presence', () => ({
userId: { eq: userId },
}))
}, [editor, userId])
const latestPresence = useValue(
`latestPresence:${userId}`,
() => {
return $presences
.get()
.slice()
.sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0]
return editor.getCollaborators().find((c) => c.userId === userId)
},
[]
[editor]
)
return latestPresence ?? null

Wyświetl plik

@ -1,4 +1,5 @@
import { useMemo } from 'react'
import { RIGHT_MOUSE_BUTTON } from '../constants'
import { TLSelectionHandle } from '../editor/types/selection-types'
import {
loopToHtmlElement,
@ -18,7 +19,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) {
const onPointerDown: React.PointerEventHandler = (e) => {
if ((e as any).isKilled) return
if (e.button === 2) {
if (e.button === RIGHT_MOUSE_BUTTON) {
editor.dispatch({
type: 'pointer',
target: 'selection',

Wyświetl plik

@ -39,12 +39,13 @@ export class Mat {
equals(m: Mat | MatModel) {
return (
this.a === m.a &&
this.b === m.b &&
this.c === m.c &&
this.d === m.d &&
this.e === m.e &&
this.f === m.f
this === m ||
(this.a === m.a &&
this.b === m.b &&
this.c === m.c &&
this.d === m.d &&
this.e === m.e &&
this.f === m.f)
)
}

Wyświetl plik

@ -4,7 +4,7 @@ import { HistoryBuffer } from './HistoryBuffer'
import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'
import { GLOBAL_START_EPOCH } from './constants'
import { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers'
import { getGlobalEpoch } from './transactions'
import { getGlobalEpoch, getIsReacting } from './transactions'
import { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
import { logComputedGetterWarning } from './warnings'
@ -189,8 +189,15 @@ class __UNSAFE__Computed<Value, Diff = unknown> implements Computed<Value, Diff>
__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value {
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH
if (!isNew && (this.lastCheckedEpoch === getGlobalEpoch() || !haveParentsChanged(this))) {
this.lastCheckedEpoch = getGlobalEpoch()
const globalEpoch = getGlobalEpoch()
if (
!isNew &&
(this.lastCheckedEpoch === globalEpoch ||
(this.isActivelyListening && getIsReacting() && this.lastTraversedEpoch < globalEpoch) ||
!haveParentsChanged(this))
) {
this.lastCheckedEpoch = globalEpoch
if (this.error) {
if (!ignoreErrors) {
throw this.error.thrownValue

Wyświetl plik

@ -70,6 +70,10 @@ export function getGlobalEpoch() {
return inst.globalEpoch
}
export function getIsReacting() {
return inst.globalIsReacting
}
/**
* Collect all of the reactors that need to run for an atom and run them.
*

Wyświetl plik

@ -2507,9 +2507,7 @@ export function useDefaultHelpers(): {
export function useDialogs(): TLUiDialogsContextType;
// @public (undocumented)
export function useEditableText(id: TLShapeId, type: string, text: string, opts?: {
disableTab: boolean;
}): {
export function useEditableText(id: TLShapeId, type: string, text: string): {
handleBlur: () => void;
handleChange: (e: React_2.ChangeEvent<HTMLTextAreaElement>) => void;
handleDoubleClick: (e: any) => any;

Wyświetl plik

@ -15735,7 +15735,7 @@
},
{
"kind": "Content",
"text": ") => {\n id: import(\"@tldraw/editor\")."
"text": ") => {\n id: "
},
{
"kind": "Reference",
@ -15819,7 +15819,7 @@
},
{
"kind": "Content",
"text": ") => {\n id: import(\"@tldraw/editor\")."
"text": ") => {\n id: "
},
{
"kind": "Reference",
@ -15894,7 +15894,7 @@
},
{
"kind": "Content",
"text": ") => {\n id: import(\"@tldraw/editor\")."
"text": ") => {\n id: "
},
{
"kind": "Reference",
@ -15903,7 +15903,7 @@
},
{
"kind": "Content",
"text": ";\n props: {\n autoSize: boolean;\n scale?: undefined;\n };\n type: \"text\";\n } | {\n id: import(\"@tldraw/editor\")."
"text": ";\n props: {\n autoSize: boolean;\n scale?: undefined;\n };\n type: \"text\";\n } | {\n id: "
},
{
"kind": "Reference",
@ -27480,14 +27480,6 @@
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ", opts?: "
},
{
"kind": "Content",
"text": "{\n disableTab: boolean;\n}"
},
{
"kind": "Content",
"text": "): "
@ -27575,8 +27567,8 @@
],
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/useEditableText.ts",
"returnTypeTokenRange": {
"startIndex": 9,
"endIndex": 26
"startIndex": 7,
"endIndex": 24
},
"releaseTag": "Public",
"overloadIndex": 1,
@ -27604,14 +27596,6 @@
"endIndex": 6
},
"isOptional": false
},
{
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"isOptional": true
}
],
"name": "useEditableText"

Wyświetl plik

@ -109,13 +109,10 @@ export function Tldraw(props: TldrawProps) {
)
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
if (preloadingError) {
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
}
if (!preloadingComplete) {
return <LoadingScreen>Loading assets...</LoadingScreen>
}

Wyświetl plik

@ -152,7 +152,9 @@ export function registerDefaultExternalContentHandlers(
editor.registerExternalContentHandler('svg-text', async ({ point, text }) => {
const position =
point ??
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center)
const svg = new DOMParser().parseFromString(text, 'image/svg+xml').querySelector('svg')
if (!svg) {
@ -185,7 +187,9 @@ export function registerDefaultExternalContentHandlers(
editor.registerExternalContentHandler('embed', ({ point, url, embed }) => {
const position =
point ??
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center)
const { width, height } = embed
@ -210,7 +214,9 @@ export function registerDefaultExternalContentHandlers(
editor.registerExternalContentHandler('files', async ({ point, files }) => {
const position =
point ??
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center)
const pagePoint = new Vec(position.x, position.y)
@ -266,7 +272,9 @@ export function registerDefaultExternalContentHandlers(
editor.registerExternalContentHandler('text', async ({ point, text }) => {
const p =
point ??
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center)
const defaultProps = editor.getShapeUtil<TLTextShape>('text').getDefaultProps()
@ -370,7 +378,9 @@ export function registerDefaultExternalContentHandlers(
const position =
point ??
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
(editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center)
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
const shape = createEmptyBookmarkShape(editor, url, position)

Wyświetl plik

@ -268,14 +268,16 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
const debugGeom: Geometry2d[] = []
const info = editor.getArrowInfo(shape)!
const hasStartBinding = shape.props.start.type === 'binding'
const hasEndBinding = shape.props.end.type === 'binding'
const hasStartArrowhead = info.start.arrowhead !== 'none'
const hasEndArrowhead = info.end.arrowhead !== 'none'
if (info.isStraight) {
const range = getStraightArrowLabelRange(editor, shape, info)
let clampedPosition = clamp(
shape.props.labelPosition,
hasStartArrowhead ? range.start : 0,
hasEndArrowhead ? range.end : 1
hasStartArrowhead || hasStartBinding ? range.start : 0,
hasEndArrowhead || hasEndBinding ? range.end : 1
)
// This makes the position snap in the middle.
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
@ -285,8 +287,8 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
if (range.dbg) debugGeom.push(...range.dbg)
let clampedPosition = clamp(
shape.props.labelPosition,
hasStartArrowhead ? range.start : 0,
hasEndArrowhead ? range.end : 1
hasStartArrowhead || hasStartBinding ? range.start : 0,
hasEndArrowhead || hasEndBinding ? range.end : 1
)
// This makes the position snap in the middle.
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition

Wyświetl plik

@ -35,7 +35,6 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
labelColor={theme[labelColor].solid}
textWidth={width}
isSelected={isSelected}
disableTab
style={{
transform: `translate(${position.x}px, ${position.y}px)`,
}}

Wyświetl plik

@ -97,7 +97,7 @@ export class Drawing extends StateNode {
this.mergeNextPoint = false
}
this.updateShapes()
this.updateDrawingShape()
}
}
@ -115,7 +115,7 @@ export class Drawing extends StateNode {
}
}
}
this.updateShapes()
this.updateDrawingShape()
}
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
@ -137,7 +137,7 @@ export class Drawing extends StateNode {
}
}
this.updateShapes()
this.updateDrawingShape()
}
override onExit? = () => {
@ -281,7 +281,7 @@ export class Drawing extends StateNode {
this.initialShape = this.editor.getShape<DrawableShape>(id)
}
private updateShapes() {
private updateDrawingShape() {
const { initialShape } = this
const { inputs } = this.editor

Wyświetl plik

@ -402,7 +402,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
</SVGContainer>
{showHtmlContainer && (
<HTMLContainer
id={shape.id}
style={{
overflow: 'hidden',
width: shape.props.w,
@ -421,7 +420,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
text={text}
isSelected={isSelected}
labelColor={theme[props.labelColor].solid}
disableTab
wrap
/>
</HTMLContainer>

Wyświetl plik

@ -190,7 +190,6 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
isNote
isSelected={isSelected}
labelColor={theme[color].note.text}
disableTab
wrap
onKeyDown={handleKeyDown}
/>

Wyświetl plik

@ -27,7 +27,6 @@ type TextLabelProps = {
bounds?: Box
isNote?: boolean
isSelected: boolean
disableTab?: boolean
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
classNamePrefix?: string
style?: React.CSSProperties
@ -51,15 +50,13 @@ export const TextLabel = React.memo(function TextLabel({
onKeyDown: handleKeyDownCustom,
classNamePrefix,
style,
disableTab = false,
textWidth,
textHeight,
}: TextLabelProps) {
const { rInput, isEmpty, isEditing, isEditingAnything, ...editableTextRest } = useEditableText(
id,
type,
text,
{ disableTab }
text
)
const [initialText, setInitialText] = useState(text)

Wyświetl plik

@ -1,12 +1,4 @@
import {
Vec,
VecLike,
assert,
average,
precise,
shortAngleDist,
toDomPrecision,
} from '@tldraw/editor'
import { Vec, VecLike, assert, average, precise, toDomPrecision } from '@tldraw/editor'
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
import { getStrokePoints } from './getStrokePoints'
import { setStrokePointRadii } from './setStrokePointRadii'
@ -36,17 +28,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
const result: StrokePoint[][] = []
let currentPartition: StrokePoint[] = [points[0]]
for (let i = 1; i < points.length - 1; i++) {
const prevPoint = points[i - 1]
const thisPoint = points[i]
const nextPoint = points[i + 1]
const prevAngle = Vec.Angle(prevPoint.point, thisPoint.point)
const nextAngle = Vec.Angle(thisPoint.point, nextPoint.point)
// acuteness is a normalized representation of how acute the angle is.
// 1 is an infinitely thin wedge
// 0 is a straight line
const acuteness = Math.abs(shortAngleDist(prevAngle, nextAngle)) / Math.PI
if (acuteness > 0.8) {
let prevV = Vec.Sub(points[1].point, points[0].point).uni()
let nextV: Vec
let dpr: number
let prevPoint: StrokePoint, thisPoint: StrokePoint, nextPoint: StrokePoint
for (let i = 1, n = points.length; i < n - 1; i++) {
prevPoint = points[i - 1]
thisPoint = points[i]
nextPoint = points[i + 1]
nextV = Vec.Sub(nextPoint.point, thisPoint.point).uni()
dpr = Vec.Dpr(prevV, nextV)
prevV = nextV
if (dpr < -0.8) {
// always treat such acute angles as elbows
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
const elbowPoint = {
@ -59,19 +54,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
continue
}
currentPartition.push(thisPoint)
if (acuteness < 0.25) {
// this is not an elbow, bail out
if (dpr > 0.7) {
// Not an elbow
continue
}
// so now we have a reasonably acute angle but it might not be an elbow if it's far
// away from it's neighbors
const avgRadius = (prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3
const incomingNormalizedDist = Vec.Dist(prevPoint.point, thisPoint.point) / avgRadius
const outgoingNormalizedDist = Vec.Dist(thisPoint.point, nextPoint.point) / avgRadius
// angular dist is a normalized representation of how far away the point is from it's neighbors
// away from it's neighbors, angular dist is a normalized representation of how far away the point is from it's neighbors
// (normalized by the radius)
const angularDist = incomingNormalizedDist + outgoingNormalizedDist
if (angularDist < 1.5) {
if (
(Vec.Dist2(prevPoint.point, thisPoint.point) + Vec.Dist2(thisPoint.point, nextPoint.point)) /
((prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3) ** 2 <
1.5
) {
// if this point is kinda close to its neighbors and it has a reasonably
// acute angle, it's probably a hard elbow
currentPartition.push(thisPoint)
@ -89,11 +85,13 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
function cleanUpPartition(partition: StrokePoint[]) {
// clean up start of partition (remove points that are too close to the start)
const startPoint = partition[0]
let nextPoint: StrokePoint
while (partition.length > 2) {
const nextPoint = partition[1]
const dist = Vec.Dist(startPoint.point, nextPoint.point)
const avgRadius = (startPoint.radius + nextPoint.radius) / 2
if (dist < avgRadius * 0.5) {
nextPoint = partition[1]
if (
Vec.Dist2(startPoint.point, nextPoint.point) <
(((startPoint.radius + nextPoint.radius) / 2) * 0.5) ** 2
) {
partition.splice(1, 1)
} else {
break
@ -101,11 +99,13 @@ function cleanUpPartition(partition: StrokePoint[]) {
}
// clean up end of partition in the same fashion
const endPoint = partition[partition.length - 1]
let prevPoint: StrokePoint
while (partition.length > 2) {
const prevPoint = partition[partition.length - 2]
const dist = Vec.Dist(endPoint.point, prevPoint.point)
const avgRadius = (endPoint.radius + prevPoint.radius) / 2
if (dist < avgRadius * 0.5) {
prevPoint = partition[partition.length - 2]
if (
Vec.Dist2(endPoint.point, prevPoint.point) <
(((endPoint.radius + prevPoint.radius) / 2) * 0.5) ** 2
) {
partition.splice(partition.length - 2, 1)
} else {
break
@ -115,13 +115,14 @@ function cleanUpPartition(partition: StrokePoint[]) {
if (partition.length > 1) {
partition[0] = {
...partition[0],
vector: Vec.FromAngle(Vec.Angle(partition[1].point, partition[0].point)),
vector: Vec.Sub(partition[0].point, partition[1].point).uni(),
}
partition[partition.length - 1] = {
...partition[partition.length - 1],
vector: Vec.FromAngle(
Vec.Angle(partition[partition.length - 1].point, partition[partition.length - 2].point)
),
vector: Vec.Sub(
partition[partition.length - 2].point,
partition[partition.length - 1].point
).uni(),
}
}
return partition

Wyświetl plik

@ -2,7 +2,6 @@ import {
TLShapeId,
TLUnknownShape,
getPointerInfo,
preventDefault,
stopEventPropagation,
useEditor,
useValue,
@ -11,31 +10,14 @@ import React, { useCallback, useEffect, useRef } from 'react'
import { INDENT, TextHelpers } from './TextHelpers'
/** @public */
export function useEditableText(
id: TLShapeId,
type: string,
text: string,
opts = { disableTab: false } as { disableTab: boolean }
) {
export function useEditableText(id: TLShapeId, type: string, text: string) {
const editor = useEditor()
const rInput = useRef<HTMLTextAreaElement>(null)
const isEditing = useValue(
'isEditing',
() => {
return editor.getEditingShapeId() === id
},
[editor]
)
const isEditingAnything = useValue(
'isEditingAnything',
() => {
return editor.getEditingShapeId() !== null
},
[editor]
)
const rSelectionRanges = useRef<Range[] | null>()
const isEditing = useValue('isEditing', () => editor.getEditingShapeId() === id, [editor])
const isEditingAnything = useValue('isEditingAnything', () => !!editor.getEditingShapeId(), [
editor,
])
useEffect(() => {
function selectAllIfEditing({ shapeId }: { shapeId: TLShapeId }) {
@ -52,14 +34,13 @@ export function useEditableText(
}
})
}
editor.on('select-all-text', selectAllIfEditing)
return () => {
editor.off('select-all-text', selectAllIfEditing)
}
}, [editor, id])
const rSelectionRanges = useRef<Range[] | null>()
useEffect(() => {
if (!isEditing) return
@ -69,10 +50,18 @@ export function useEditableText(
// Focus if we're not already focused
if (document.activeElement !== elm) {
elm.focus()
// On mobile etc, just select all the text when we start focusing
if (editor.getInstanceState().isCoarsePointer) {
elm.select()
}
} else {
// This fixes iOS not showing the cursor sometimes. This "shakes" the cursor
// awake.
if (editor.environment.isSafari) {
elm.blur()
elm.focus()
}
}
// When the selection changes, save the selection ranges
@ -103,12 +92,14 @@ export function useEditableText(
requestAnimationFrame(() => {
const elm = rInput.current
const editingShapeId = editor.getEditingShapeId()
// Did we move to a different shape?
if (editingShapeId) {
// important! these ^v are two different things
// is that shape OUR shape?
if (elm && editingShapeId === id) {
elm.focus()
if (ranges && ranges.length) {
const selection = window.getSelection()
if (selection) {
@ -134,20 +125,9 @@ export function useEditableText(
}
break
}
case 'Tab': {
if (!opts.disableTab) {
preventDefault(e)
if (e.shiftKey) {
TextHelpers.unindent(e.currentTarget)
} else {
TextHelpers.indent(e.currentTarget)
}
}
break
}
}
},
[editor, id, opts.disableTab]
[editor, id]
)
// When the text changes, update the text value.
@ -198,8 +178,6 @@ export function useEditableText(
[editor, id, isEditing]
)
const handleDoubleClick = stopEventPropagation
return {
rInput,
handleFocus: noop,
@ -207,7 +185,7 @@ export function useEditableText(
handleKeyDown,
handleChange,
handleInputPointerDown,
handleDoubleClick,
handleDoubleClick: stopEventPropagation,
isEmpty: text.trim().length === 0,
isEditing,
isEditingAnything,

Wyświetl plik

@ -7,18 +7,22 @@ import {
SvgExportContext,
TLOnEditEndHandler,
TLOnResizeHandler,
TLShapeId,
TLShapeUtilFlag,
TLTextShape,
Vec,
WeakMapCache,
getDefaultColorTheme,
preventDefault,
textShapeMigrations,
textShapeProps,
toDomPrecision,
useEditor,
} from '@tldraw/editor'
import { useCallback } from 'react'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextHelpers } from '../shared/TextHelpers'
import { TextLabel } from '../shared/TextLabel'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
import { getFontDefForExport } from '../shared/defaultStyleDefs'
@ -73,6 +77,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
const { width, height } = this.getMinDimensions(shape)
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
const theme = useDefaultColorTheme()
const handleKeyDown = useTextShapeKeydownHandler(id)
return (
<TextLabel
@ -94,6 +99,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
transformOrigin: 'top left',
}}
wrap
onKeyDown={handleKeyDown}
/>
)
}
@ -332,3 +338,32 @@ function getTextSize(editor: Editor, props: TLTextShape['props']) {
height: Math.max(fontSize, result.h),
}
}
function useTextShapeKeydownHandler(id: TLShapeId) {
const editor = useEditor()
return useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (editor.getEditingShapeId() !== id) return
switch (e.key) {
case 'Enter': {
if (e.ctrlKey || e.metaKey) {
editor.complete()
}
break
}
case 'Tab': {
preventDefault(e)
if (e.shiftKey) {
TextHelpers.unindent(e.currentTarget)
} else {
TextHelpers.indent(e.currentTarget)
}
break
}
}
},
[editor, id]
)
}

Wyświetl plik

@ -92,8 +92,8 @@ export class PointingArrowLabel extends StateNode {
let nextLabelPosition
if (info.isStraight) {
// straight arrows
const lineLength = Vec.Dist2(info.start.point, info.end.point)
const segmentLength = Vec.Dist2(info.end.point, nearestPoint)
const lineLength = Vec.Dist(info.start.point, info.end.point)
const segmentLength = Vec.Dist(info.end.point, nearestPoint)
nextLabelPosition = 1 - segmentLength / lineLength
} else {
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d

Wyświetl plik

@ -117,7 +117,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
editor.putExternalContent({
type: 'embed',
url,
point: editor.getViewportPageCenter(),
point: editor.getViewportPageBounds().center,
embed: embedInfoForUrl.definition,
})

Wyświetl plik

@ -1,5 +1,5 @@
import { useEditor } from '@tldraw/editor'
import { useEffect, useState } from 'react'
import { useEditor, useQuickReactor } from '@tldraw/editor'
import { useRef, useState } from 'react'
import { useActions } from '../../context/actions'
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
@ -9,33 +9,25 @@ export function BackToContent() {
const actions = useActions()
const [showBackToContent, setShowBackToContent] = useState(false)
const rIsShowing = useRef(false)
useEffect(() => {
let showBackToContentPrev = false
const interval = setInterval(() => {
const renderingShapes = editor.getRenderingShapes()
const renderingBounds = editor.getRenderingBounds()
// Rendering shapes includes all the shapes in the current page.
// We have to filter them down to just the shapes that are inside the renderingBounds.
const visibleShapes = renderingShapes.filter((s) => {
const maskedPageBounds = editor.getShapeMaskedPageBounds(s.id)
return maskedPageBounds && renderingBounds.includes(maskedPageBounds)
})
const showBackToContentNow =
visibleShapes.length === 0 && editor.getCurrentPageShapes().length > 0
useQuickReactor(
'toggle showback to content',
() => {
const showBackToContentPrev = rIsShowing.current
const shapeIds = editor.getCurrentPageShapeIds()
let showBackToContentNow = false
if (shapeIds.size) {
showBackToContentNow = shapeIds.size === editor.getCulledShapes().size
}
if (showBackToContentPrev !== showBackToContentNow) {
setShowBackToContent(showBackToContentNow)
showBackToContentPrev = showBackToContentNow
rIsShowing.current = showBackToContentNow
}
}, 1000)
return () => {
clearInterval(interval)
}
}, [editor])
},
[editor]
)
if (!showBackToContent) return null

Wyświetl plik

@ -1,18 +1,13 @@
import {
ANIMATION_MEDIUM_MS,
Box,
TLPointerEventInfo,
TLShapeId,
Vec,
getPointerInfo,
intersectPolygonPolygon,
normalizeWheel,
releasePointerCapture,
setPointerCapture,
useComputed,
useEditor,
useIsDarkMode,
useQuickReactor,
} from '@tldraw/editor'
import * as React from 'react'
import { MinimapManager } from './MinimapManager'
@ -24,67 +19,78 @@ export function DefaultMinimap() {
const rCanvas = React.useRef<HTMLCanvasElement>(null!)
const rPointing = React.useRef(false)
const isDarkMode = useIsDarkMode()
const devicePixelRatio = useComputed('dpr', () => editor.getInstanceState().devicePixelRatio, [
editor,
])
const presences = React.useMemo(() => editor.store.query.records('instance_presence'), [editor])
const minimap = React.useMemo(() => new MinimapManager(editor), [editor])
const minimapRef = React.useRef<MinimapManager>()
React.useEffect(() => {
// Must check after render
const raf = requestAnimationFrame(() => {
minimap.updateColors()
minimap.render()
})
return () => {
cancelAnimationFrame(raf)
}
}, [editor, minimap, isDarkMode])
const minimap = new MinimapManager(editor, rCanvas.current)
minimapRef.current = minimap
return minimapRef.current.close
}, [editor])
const onDoubleClick = React.useCallback(
(e: React.MouseEvent<HTMLCanvasElement>) => {
if (!editor.getCurrentPageShapeIds().size) return
if (!minimapRef.current) return
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
const point = minimapRef.current.minimapScreenPointToPagePoint(
e.clientX,
e.clientY,
false,
false
)
const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true)
const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint(
e.clientX,
e.clientY,
false,
true
)
minimap.originPagePoint.setTo(clampedPoint)
minimap.originPageCenter.setTo(editor.getViewportPageBounds().center)
minimapRef.current.originPagePoint.setTo(clampedPoint)
minimapRef.current.originPageCenter.setTo(editor.getViewportPageBounds().center)
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
},
[editor, minimap]
[editor]
)
const onPointerDown = React.useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
if (!minimapRef.current) return
const elm = e.currentTarget
setPointerCapture(elm, e)
if (!editor.getCurrentPageShapeIds().size) return
rPointing.current = true
minimap.isInViewport = false
minimapRef.current.isInViewport = false
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
const point = minimapRef.current.minimapScreenPointToPagePoint(
e.clientX,
e.clientY,
false,
false
)
const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true)
const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint(
e.clientX,
e.clientY,
false,
true
)
const _vpPageBounds = editor.getViewportPageBounds()
minimap.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
minimapRef.current.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
if (minimap.isInViewport) {
minimap.originPagePoint.setTo(clampedPoint)
minimap.originPageCenter.setTo(_vpPageBounds.center)
if (minimapRef.current.isInViewport) {
minimapRef.current.originPagePoint.setTo(clampedPoint)
minimapRef.current.originPageCenter.setTo(_vpPageBounds.center)
} else {
const delta = Vec.Sub(_vpPageBounds.center, _vpPageBounds.point)
const pagePoint = Vec.Add(point, delta)
minimap.originPagePoint.setTo(pagePoint)
minimap.originPageCenter.setTo(point)
minimapRef.current.originPagePoint.setTo(pagePoint)
minimapRef.current.originPageCenter.setTo(point)
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
}
@ -98,16 +104,24 @@ export function DefaultMinimap() {
document.body.addEventListener('pointerup', release)
},
[editor, minimap]
[editor]
)
const onPointerMove = React.useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, e.shiftKey, true)
if (!minimapRef.current) return
const point = minimapRef.current.minimapScreenPointToPagePoint(
e.clientX,
e.clientY,
e.shiftKey,
true
)
if (rPointing.current) {
if (minimap.isInViewport) {
const delta = minimap.originPagePoint.clone().sub(minimap.originPageCenter)
if (minimapRef.current.isInViewport) {
const delta = minimapRef.current.originPagePoint
.clone()
.sub(minimapRef.current.originPageCenter)
editor.centerOnPoint(Vec.Sub(point, delta))
return
}
@ -115,7 +129,7 @@ export function DefaultMinimap() {
editor.centerOnPoint(point)
}
const pagePoint = minimap.getPagePoint(e.clientX, e.clientY)
const pagePoint = minimapRef.current.getPagePoint(e.clientX, e.clientY)
const screenPoint = editor.pageToScreen(pagePoint)
@ -130,7 +144,7 @@ export function DefaultMinimap() {
editor.dispatch(info)
},
[editor, minimap]
[editor]
)
const onWheel = React.useCallback(
@ -150,73 +164,16 @@ export function DefaultMinimap() {
[editor]
)
// Update the minimap's dpr when the dpr changes
useQuickReactor(
'update when dpr changes',
() => {
const dpr = devicePixelRatio.get()
minimap.setDpr(dpr)
const isDarkMode = useIsDarkMode()
const canvas = rCanvas.current as HTMLCanvasElement
const rect = canvas.getBoundingClientRect()
const width = rect.width * dpr
const height = rect.height * dpr
// These must happen in order
canvas.width = width
canvas.height = height
minimap.canvasScreenBounds.set(rect.x, rect.y, width, height)
minimap.cvs = rCanvas.current
},
[devicePixelRatio, minimap]
)
useQuickReactor(
'minimap render when pagebounds or collaborators changes',
() => {
const shapeIdsOnCurrentPage = editor.getCurrentPageShapeIds()
const commonBoundsOfAllShapesOnCurrentPage = editor.getCurrentPageBounds()
const viewportPageBounds = editor.getViewportPageBounds()
const _dpr = devicePixelRatio.get() // dereference
minimap.contentPageBounds = commonBoundsOfAllShapesOnCurrentPage
? Box.Expand(commonBoundsOfAllShapesOnCurrentPage, viewportPageBounds)
: viewportPageBounds
minimap.updateContentScreenBounds()
// All shape bounds
const allShapeBounds = [] as (Box & { id: TLShapeId })[]
shapeIdsOnCurrentPage.forEach((id) => {
let pageBounds = editor.getShapePageBounds(id) as Box & { id: TLShapeId }
if (!pageBounds) return
const pageMask = editor.getShapeMask(id)
if (pageMask) {
const intersection = intersectPolygonPolygon(pageMask, pageBounds.corners)
if (!intersection) {
return
}
pageBounds = Box.FromPoints(intersection) as Box & { id: TLShapeId }
}
if (pageBounds) {
pageBounds.id = id // kinda dirty but we want to include the id here
allShapeBounds.push(pageBounds)
}
})
minimap.pageBounds = allShapeBounds
minimap.collaborators = presences.get()
minimap.render()
},
[editor, minimap]
)
React.useEffect(() => {
// need to wait a tick for next theme css to be applied
// otherwise the minimap will render with the wrong colors
setTimeout(() => {
minimapRef.current?.updateColors()
minimapRef.current?.render()
})
}, [isDarkMode])
return (
<div className="tlui-minimap">

Wyświetl plik

@ -1,114 +1,159 @@
import {
Box,
ComputedCache,
Editor,
PI2,
TLInstancePresence,
TLShapeId,
TLShape,
Vec,
atom,
clamp,
computed,
react,
uniqueId,
} from '@tldraw/editor'
import { getRgba } from './getRgba'
import { BufferStuff, appendVertices, setupWebGl } from './minimap-webgl-setup'
import { pie, rectangle, roundedRectangle } from './minimap-webgl-shapes'
export class MinimapManager {
constructor(public editor: Editor) {}
dpr = 1
colors = {
shapeFill: 'rgba(144, 144, 144, .1)',
selectFill: '#2f80ed',
viewportFill: 'rgba(144, 144, 144, .1)',
disposables = [] as (() => void)[]
close = () => this.disposables.forEach((d) => d())
gl: ReturnType<typeof setupWebGl>
shapeGeometryCache: ComputedCache<Float32Array | null, TLShape>
constructor(
public editor: Editor,
public readonly elem: HTMLCanvasElement
) {
this.gl = setupWebGl(elem)
this.shapeGeometryCache = editor.store.createComputedCache('webgl-geometry', (r: TLShape) => {
const bounds = editor.getShapeMaskedPageBounds(r.id)
if (!bounds) return null
const arr = new Float32Array(12)
rectangle(arr, 0, bounds.x, bounds.y, bounds.w, bounds.h)
return arr
})
this.colors = this._getColors()
this.disposables.push(this._listenForCanvasResize(), react('minimap render', this.render))
}
id = uniqueId()
cvs: HTMLCanvasElement | null = null
pageBounds: (Box & { id: TLShapeId })[] = []
collaborators: TLInstancePresence[] = []
private _getColors() {
const style = getComputedStyle(this.editor.getContainer())
canvasScreenBounds = new Box()
canvasPageBounds = new Box()
return {
shapeFill: getRgba(style.getPropertyValue('--color-text-3').trim()),
selectFill: getRgba(style.getPropertyValue('--color-selected').trim()),
viewportFill: getRgba(style.getPropertyValue('--color-muted-1').trim()),
}
}
contentPageBounds = new Box()
contentScreenBounds = new Box()
private colors: ReturnType<MinimapManager['_getColors']>
// this should be called after dark/light mode changes have propagated to the dom
updateColors() {
this.colors = this._getColors()
}
readonly id = uniqueId()
@computed
getDpr() {
return this.editor.getInstanceState().devicePixelRatio
}
@computed
getContentPageBounds() {
const viewportPageBounds = this.editor.getViewportPageBounds()
const commonShapeBounds = this.editor.getCurrentPageBounds()
return commonShapeBounds
? Box.Expand(commonShapeBounds, viewportPageBounds)
: viewportPageBounds
}
@computed
getContentScreenBounds() {
const contentPageBounds = this.getContentPageBounds()
const topLeft = this.editor.pageToScreen(contentPageBounds.point)
const bottomRight = this.editor.pageToScreen(
new Vec(contentPageBounds.maxX, contentPageBounds.maxY)
)
return new Box(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
}
private _getCanvasBoundingRect() {
const { x, y, width, height } = this.elem.getBoundingClientRect()
return new Box(x, y, width, height)
}
private readonly canvasBoundingClientRect = atom('canvasBoundingClientRect', new Box())
getCanvasScreenBounds() {
return this.canvasBoundingClientRect.get()
}
private _listenForCanvasResize() {
const observer = new ResizeObserver(() => {
const rect = this._getCanvasBoundingRect()
this.canvasBoundingClientRect.set(rect)
})
observer.observe(this.elem)
return () => observer.disconnect()
}
@computed
getCanvasSize() {
const rect = this.canvasBoundingClientRect.get()
const dpr = this.getDpr()
return new Vec(rect.width * dpr, rect.height * dpr)
}
@computed
getCanvasClientPosition() {
return this.canvasBoundingClientRect.get().point
}
originPagePoint = new Vec()
originPageCenter = new Vec()
isInViewport = false
debug = false
/** Get the canvas's true bounds converted to page bounds. */
@computed getCanvasPageBounds() {
const canvasScreenBounds = this.getCanvasScreenBounds()
const contentPageBounds = this.getContentPageBounds()
setDpr(dpr: number) {
this.dpr = +dpr.toFixed(2)
}
const aspectRatio = canvasScreenBounds.width / canvasScreenBounds.height
updateContentScreenBounds = () => {
const { contentScreenBounds, contentPageBounds: content, canvasScreenBounds: canvas } = this
let { x, y, w, h } = contentScreenBounds
if (content.w > content.h) {
const sh = canvas.w / (content.w / content.h)
if (sh > canvas.h) {
x = (canvas.w - canvas.w * (canvas.h / sh)) / 2
y = 0
w = canvas.w * (canvas.h / sh)
h = canvas.h
} else {
x = 0
y = (canvas.h - sh) / 2
w = canvas.w
h = sh
}
} else if (content.w < content.h) {
const sw = canvas.h / (content.h / content.w)
x = (canvas.w - sw) / 2
y = 0
w = sw
h = canvas.h
} else {
x = canvas.h / 2
y = 0
w = canvas.h
h = canvas.h
let targetWidth = contentPageBounds.width
let targetHeight = targetWidth / aspectRatio
if (targetHeight < contentPageBounds.height) {
targetHeight = contentPageBounds.height
targetWidth = targetHeight * aspectRatio
}
contentScreenBounds.set(x, y, w, h)
const box = new Box(0, 0, targetWidth, targetHeight)
box.center = contentPageBounds.center
return box
}
/** Get the canvas's true bounds converted to page bounds. */
updateCanvasPageBounds = () => {
const { canvasPageBounds, canvasScreenBounds, contentPageBounds, contentScreenBounds } = this
canvasPageBounds.set(
0,
0,
contentPageBounds.width / (contentScreenBounds.width / canvasScreenBounds.width),
contentPageBounds.height / (contentScreenBounds.height / canvasScreenBounds.height)
)
canvasPageBounds.center = contentPageBounds.center
@computed getCanvasPageBoundsArray() {
const { x, y, w, h } = this.getCanvasPageBounds()
return new Float32Array([x, y, w, h])
}
getScreenPoint = (x: number, y: number) => {
const { canvasScreenBounds } = this
getPagePoint = (clientX: number, clientY: number) => {
const canvasPageBounds = this.getCanvasPageBounds()
const canvasScreenBounds = this.getCanvasScreenBounds()
const screenX = (x - canvasScreenBounds.minX) * this.dpr
const screenY = (y - canvasScreenBounds.minY) * this.dpr
// first offset the canvas position
let x = clientX - canvasScreenBounds.x
let y = clientY - canvasScreenBounds.y
return { x: screenX, y: screenY }
}
// then multiply by the ratio between the page and screen bounds
x *= canvasPageBounds.width / canvasScreenBounds.width
y *= canvasPageBounds.height / canvasScreenBounds.height
getPagePoint = (x: number, y: number) => {
const { contentPageBounds, contentScreenBounds, canvasPageBounds } = this
// then add the canvas page bounds' offset
x += canvasPageBounds.minX
y += canvasPageBounds.minY
const { x: screenX, y: screenY } = this.getScreenPoint(x, y)
return new Vec(
canvasPageBounds.minX + (screenX * contentPageBounds.width) / contentScreenBounds.width,
canvasPageBounds.minY + (screenY * contentPageBounds.height) / contentScreenBounds.height,
1
)
return new Vec(x, y, 1)
}
minimapScreenPointToPagePoint = (
@ -123,13 +168,13 @@ export class MinimapManager {
let { x: px, y: py } = this.getPagePoint(x, y)
if (clampToBounds) {
const shapesPageBounds = this.editor.getCurrentPageBounds()
const shapesPageBounds = this.editor.getCurrentPageBounds() ?? new Box()
const vpPageBounds = viewportPageBounds
const minX = (shapesPageBounds?.minX ?? 0) - vpPageBounds.width / 2
const maxX = (shapesPageBounds?.maxX ?? 0) + vpPageBounds.width / 2
const minY = (shapesPageBounds?.minY ?? 0) - vpPageBounds.height / 2
const maxY = (shapesPageBounds?.maxY ?? 0) + vpPageBounds.height / 2
const minX = shapesPageBounds.minX - vpPageBounds.width / 2
const maxX = shapesPageBounds.maxX + vpPageBounds.width / 2
const minY = shapesPageBounds.minY - vpPageBounds.height / 2
const maxY = shapesPageBounds.maxY + vpPageBounds.height / 2
const lx = Math.max(0, minX + vpPageBounds.width - px)
const rx = Math.max(0, -(maxX - vpPageBounds.width - px))
@ -171,209 +216,110 @@ export class MinimapManager {
return new Vec(px, py)
}
updateColors = () => {
const style = getComputedStyle(this.editor.getContainer())
this.colors = {
shapeFill: style.getPropertyValue('--color-text-3').trim(),
selectFill: style.getPropertyValue('--color-selected').trim(),
viewportFill: style.getPropertyValue('--color-muted-1').trim(),
}
}
render = () => {
const { cvs, pageBounds } = this
this.updateCanvasPageBounds()
// make sure we update when dark mode switches
const context = this.gl.context
const canvasSize = this.getCanvasSize()
const { editor, canvasScreenBounds, canvasPageBounds, contentPageBounds, contentScreenBounds } =
this
const { width: cw, height: ch } = canvasScreenBounds
this.gl.setCanvasPageBounds(this.getCanvasPageBoundsArray())
const selectedShapeIds = new Set(editor.getSelectedShapeIds())
const viewportPageBounds = editor.getViewportPageBounds()
this.elem.width = canvasSize.x
this.elem.height = canvasSize.y
context.viewport(0, 0, canvasSize.x, canvasSize.y)
if (!cvs || !pageBounds) {
return
// this affects which color transparent shapes are blended with
// during rendering. If we were to invert this any shapes narrower
// than 1 px in screen space would have much lower contrast. e.g.
// draw shapes on a large canvas.
if (this.editor.user.getIsDarkMode()) {
context.clearColor(1, 1, 1, 0)
} else {
context.clearColor(0, 0, 0, 0)
}
const ctx = cvs.getContext('2d')!
context.clear(context.COLOR_BUFFER_BIT)
if (!ctx) {
throw new Error('Minimap (shapes): Could not get context')
}
const selectedShapes = new Set(this.editor.getSelectedShapeIds())
ctx.resetTransform()
ctx.globalAlpha = 1
ctx.clearRect(0, 0, cw, ch)
const colors = this.colors
let selectedShapeOffset = 0
let unselectedShapeOffset = 0
// Transform canvas
const ids = this.editor.getCurrentPageShapeIdsSorted()
const sx = contentScreenBounds.width / contentPageBounds.width
const sy = contentScreenBounds.height / contentPageBounds.height
for (let i = 0, len = ids.length; i < len; i++) {
const shapeId = ids[i]
const geometry = this.shapeGeometryCache.get(shapeId)
if (!geometry) continue
ctx.translate((cw - contentScreenBounds.width) / 2, (ch - contentScreenBounds.height) / 2)
ctx.scale(sx, sy)
ctx.translate(-contentPageBounds.minX, -contentPageBounds.minY)
const len = geometry.length
// shapes
const shapesPath = new Path2D()
const selectedPath = new Path2D()
const { shapeFill, selectFill, viewportFill } = this.colors
// When there are many shapes, don't draw rounded rectangles;
// consider using the shape's size instead.
let pb: Box & { id: TLShapeId }
for (let i = 0, n = pageBounds.length; i < n; i++) {
pb = pageBounds[i]
;(selectedShapeIds.has(pb.id) ? selectedPath : shapesPath).rect(
pb.minX,
pb.minY,
pb.width,
pb.height
)
}
// Fill the shapes paths
ctx.fillStyle = shapeFill
ctx.fill(shapesPath)
// Fill the selected paths
ctx.fillStyle = selectFill
ctx.fill(selectedPath)
if (this.debug) {
// Page bounds
const commonBounds = Box.Common(pageBounds)
const { minX, minY, width, height } = commonBounds
ctx.strokeStyle = 'green'
ctx.lineWidth = 2 / sx
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
}
// Brush
{
const { brush } = editor.getInstanceState()
if (brush) {
const { x, y, w, h } = brush
ctx.beginPath()
MinimapManager.sharpRect(ctx, x, y, w, h)
ctx.closePath()
ctx.fillStyle = viewportFill
ctx.fill()
if (selectedShapes.has(shapeId)) {
appendVertices(this.gl.selectedShapes, selectedShapeOffset, geometry)
selectedShapeOffset += len
} else {
appendVertices(this.gl.unselectedShapes, unselectedShapeOffset, geometry)
unselectedShapeOffset += len
}
}
// Viewport
{
const { minX, minY, width, height } = viewportPageBounds
ctx.beginPath()
const rx = 12 / sx
const ry = 12 / sx
MinimapManager.roundedRect(
ctx,
minX,
minY,
width,
height,
Math.min(width / 4, rx),
Math.min(height / 4, ry)
)
ctx.closePath()
ctx.fillStyle = viewportFill
ctx.fill()
if (this.debug) {
ctx.strokeStyle = 'orange'
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
}
}
// Show collaborator cursors
// Padding for canvas bounds edges
const px = 2.5 / sx
const py = 2.5 / sy
const currentPageId = editor.getCurrentPageId()
let collaborator: TLInstancePresence
for (let i = 0; i < this.collaborators.length; i++) {
collaborator = this.collaborators[i]
if (collaborator.currentPageId !== currentPageId) {
continue
}
ctx.beginPath()
ctx.ellipse(
clamp(collaborator.cursor.x, canvasPageBounds.minX + px, canvasPageBounds.maxX - px),
clamp(collaborator.cursor.y, canvasPageBounds.minY + py, canvasPageBounds.maxY - py),
5 / sx,
5 / sy,
0,
0,
PI2
)
ctx.fillStyle = collaborator.color
ctx.fill()
}
if (this.debug) {
ctx.lineWidth = 2 / sx
{
// Minimap Bounds
const { minX, minY, width, height } = contentPageBounds
ctx.strokeStyle = 'red'
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
}
{
// Canvas Bounds
const { minX, minY, width, height } = canvasPageBounds
ctx.strokeStyle = 'blue'
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
}
}
this.drawViewport()
this.drawShapes(this.gl.unselectedShapes, unselectedShapeOffset, colors.shapeFill)
this.drawShapes(this.gl.selectedShapes, selectedShapeOffset, colors.selectFill)
this.drawCollaborators()
}
static roundedRect(
ctx: CanvasRenderingContext2D | Path2D,
x: number,
y: number,
width: number,
height: number,
rx: number,
ry: number
) {
if (rx < 1 && ry < 1) {
ctx.rect(x, y, width, height)
return
}
ctx.moveTo(x + rx, y)
ctx.lineTo(x + width - rx, y)
ctx.quadraticCurveTo(x + width, y, x + width, y + ry)
ctx.lineTo(x + width, y + height - ry)
ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height)
ctx.lineTo(x + rx, y + height)
ctx.quadraticCurveTo(x, y + height, x, y + height - ry)
ctx.lineTo(x, y + ry)
ctx.quadraticCurveTo(x, y, x + rx, y)
private drawShapes(stuff: BufferStuff, len: number, color: Float32Array) {
this.gl.prepareTriangles(stuff, len)
this.gl.setFillColor(color)
this.gl.drawTriangles(len)
}
static sharpRect(
ctx: CanvasRenderingContext2D | Path2D,
x: number,
y: number,
width: number,
height: number,
_rx?: number,
_ry?: number
) {
ctx.rect(x, y, width, height)
private drawViewport() {
const viewport = this.editor.getViewportPageBounds()
const zoom = this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width
const len = roundedRectangle(this.gl.viewport.vertices, viewport, 4 * zoom)
this.gl.prepareTriangles(this.gl.viewport, len)
this.gl.setFillColor(this.colors.viewportFill)
this.gl.drawTriangles(len)
}
drawCollaborators() {
const collaborators = this.editor.getCollaboratorsOnCurrentPage()
if (!collaborators.length) return
const zoom = this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width
// just draw a little circle for each collaborator
const numSegmentsPerCircle = 20
const dataSizePerCircle = numSegmentsPerCircle * 6
const totalSize = dataSizePerCircle * collaborators.length
// expand vertex array if needed
if (this.gl.collaborators.vertices.length < totalSize) {
this.gl.collaborators.vertices = new Float32Array(totalSize)
}
const vertices = this.gl.collaborators.vertices
let offset = 0
for (const { cursor } of collaborators) {
pie(vertices, {
center: Vec.From(cursor),
radius: 2 * zoom,
offset,
numArcSegments: numSegmentsPerCircle,
})
offset += dataSizePerCircle
}
this.gl.prepareTriangles(this.gl.collaborators, totalSize)
offset = 0
for (const { color } of collaborators) {
this.gl.setFillColor(getRgba(color))
this.gl.context.drawArrays(this.gl.context.TRIANGLES, offset / 2, dataSizePerCircle / 2)
offset += dataSizePerCircle
}
}
}

Wyświetl plik

@ -0,0 +1,16 @@
const memo = {} as Record<string, Float32Array>
export function getRgba(colorString: string) {
if (memo[colorString]) {
return memo[colorString]
}
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context!.fillStyle = colorString
context!.fillRect(0, 0, 1, 1)
const [r, g, b, a] = context!.getImageData(0, 0, 1, 1).data
const result = new Float32Array([r / 255, g / 255, b / 255, a / 255])
memo[colorString] = result
return result
}

Wyświetl plik

@ -0,0 +1,148 @@
import { roundedRectangleDataSize } from './minimap-webgl-shapes'
export function setupWebGl(canvas: HTMLCanvasElement | null) {
if (!canvas) throw new Error('Canvas element not found')
const context = canvas.getContext('webgl2', {
premultipliedAlpha: false,
})
if (!context) throw new Error('Failed to get webgl2 context')
const vertexShaderSourceCode = `#version 300 es
precision mediump float;
in vec2 shapeVertexPosition;
uniform vec4 canvasPageBounds;
// taken (with thanks) from
// https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
void main() {
// convert the position from pixels to 0.0 to 1.0
vec2 zeroToOne = (shapeVertexPosition - canvasPageBounds.xy) / canvasPageBounds.zw;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
}`
const vertexShader = context.createShader(context.VERTEX_SHADER)
if (!vertexShader) {
throw new Error('Failed to create vertex shader')
}
context.shaderSource(vertexShader, vertexShaderSourceCode)
context.compileShader(vertexShader)
if (!context.getShaderParameter(vertexShader, context.COMPILE_STATUS)) {
throw new Error('Failed to compile vertex shader')
}
const fragmentShaderSourceCode = `#version 300 es
precision mediump float;
uniform vec4 fillColor;
out vec4 outputColor;
void main() {
outputColor = fillColor;
}`
const fragmentShader = context.createShader(context.FRAGMENT_SHADER)
if (!fragmentShader) {
throw new Error('Failed to create fragment shader')
}
context.shaderSource(fragmentShader, fragmentShaderSourceCode)
context.compileShader(fragmentShader)
if (!context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)) {
throw new Error('Failed to compile fragment shader')
}
const program = context.createProgram()
if (!program) {
throw new Error('Failed to create program')
}
context.attachShader(program, vertexShader)
context.attachShader(program, fragmentShader)
context.linkProgram(program)
if (!context.getProgramParameter(program, context.LINK_STATUS)) {
throw new Error('Failed to link program')
}
context.useProgram(program)
const shapeVertexPositionAttributeLocation = context.getAttribLocation(
program,
'shapeVertexPosition'
)
if (shapeVertexPositionAttributeLocation < 0) {
throw new Error('Failed to get shapeVertexPosition attribute location')
}
context.enableVertexAttribArray(shapeVertexPositionAttributeLocation)
const canvasPageBoundsLocation = context.getUniformLocation(program, 'canvasPageBounds')
const fillColorLocation = context.getUniformLocation(program, 'fillColor')
const selectedShapesBuffer = context.createBuffer()
if (!selectedShapesBuffer) throw new Error('Failed to create buffer')
const unselectedShapesBuffer = context.createBuffer()
if (!unselectedShapesBuffer) throw new Error('Failed to create buffer')
return {
context,
selectedShapes: allocateBuffer(context, 1024),
unselectedShapes: allocateBuffer(context, 4096),
viewport: allocateBuffer(context, roundedRectangleDataSize),
collaborators: allocateBuffer(context, 1024),
prepareTriangles(stuff: BufferStuff, len: number) {
context.bindBuffer(context.ARRAY_BUFFER, stuff.buffer)
context.bufferData(context.ARRAY_BUFFER, stuff.vertices, context.STATIC_DRAW, 0, len)
context.enableVertexAttribArray(shapeVertexPositionAttributeLocation)
context.vertexAttribPointer(
shapeVertexPositionAttributeLocation,
2,
context.FLOAT,
false,
0,
0
)
},
drawTriangles(len: number) {
context.drawArrays(context.TRIANGLES, 0, len / 2)
},
setFillColor(color: Float32Array) {
context.uniform4fv(fillColorLocation, color)
},
setCanvasPageBounds(bounds: Float32Array) {
context.uniform4fv(canvasPageBoundsLocation, bounds)
},
}
}
export type BufferStuff = ReturnType<typeof allocateBuffer>
function allocateBuffer(context: WebGL2RenderingContext, size: number) {
const buffer = context.createBuffer()
if (!buffer) throw new Error('Failed to create buffer')
return { buffer, vertices: new Float32Array(size) }
}
export function appendVertices(bufferStuff: BufferStuff, offset: number, data: Float32Array) {
let len = bufferStuff.vertices.length
while (len < offset + data.length) {
len *= 2
}
if (len != bufferStuff.vertices.length) {
const newVertices = new Float32Array(len)
newVertices.set(bufferStuff.vertices)
bufferStuff.vertices = newVertices
}
bufferStuff.vertices.set(data, offset)
}

Wyświetl plik

@ -0,0 +1,144 @@
import { Box, HALF_PI, PI, PI2, Vec } from '@tldraw/editor'
export const numArcSegmentsPerCorner = 10
export const roundedRectangleDataSize =
// num triangles in corners
4 * 6 * numArcSegmentsPerCorner +
// num triangles in center rect
12 +
// num triangles in outer rects
4 * 12
export function pie(
array: Float32Array,
{
center,
radius,
numArcSegments = 20,
startAngle = 0,
endAngle = PI2,
offset = 0,
}: {
center: Vec
radius: number
numArcSegments?: number
startAngle?: number
endAngle?: number
offset?: number
}
) {
const angle = (endAngle - startAngle) / numArcSegments
let i = offset
for (let a = startAngle; a < endAngle; a += angle) {
array[i++] = center.x
array[i++] = center.y
array[i++] = center.x + Math.cos(a) * radius
array[i++] = center.y + Math.sin(a) * radius
array[i++] = center.x + Math.cos(a + angle) * radius
array[i++] = center.y + Math.sin(a + angle) * radius
}
return array
}
/** @internal **/
export function rectangle(
array: Float32Array,
offset: number,
x: number,
y: number,
w: number,
h: number
) {
array[offset++] = x
array[offset++] = y
array[offset++] = x
array[offset++] = y + h
array[offset++] = x + w
array[offset++] = y
array[offset++] = x + w
array[offset++] = y
array[offset++] = x
array[offset++] = y + h
array[offset++] = x + w
array[offset++] = y + h
}
export function roundedRectangle(data: Float32Array, box: Box, radius: number): number {
const numArcSegments = numArcSegmentsPerCorner
radius = Math.min(radius, Math.min(box.w, box.h) / 2)
// first draw the inner box
const innerBox = Box.ExpandBy(box, -radius)
if (innerBox.w <= 0 || innerBox.h <= 0) {
// just draw a circle
pie(data, { center: box.center, radius: radius, numArcSegments: numArcSegmentsPerCorner * 4 })
return numArcSegmentsPerCorner * 4 * 6
}
let offset = 0
// draw center rect first
rectangle(data, offset, innerBox.minX, innerBox.minY, innerBox.w, innerBox.h)
offset += 12
// then top rect
rectangle(data, offset, innerBox.minX, box.minY, innerBox.w, radius)
offset += 12
// then right rect
rectangle(data, offset, innerBox.maxX, innerBox.minY, radius, innerBox.h)
offset += 12
// then bottom rect
rectangle(data, offset, innerBox.minX, innerBox.maxY, innerBox.w, radius)
offset += 12
// then left rect
rectangle(data, offset, box.minX, innerBox.minY, radius, innerBox.h)
offset += 12
// draw the corners
// top left
pie(data, {
numArcSegments,
offset,
center: innerBox.point,
radius,
startAngle: PI,
endAngle: PI * 1.5,
})
offset += numArcSegments * 6
// top right
pie(data, {
numArcSegments,
offset,
center: Vec.Add(innerBox.point, new Vec(innerBox.w, 0)),
radius,
startAngle: PI * 1.5,
endAngle: PI2,
})
offset += numArcSegments * 6
// bottom right
pie(data, {
numArcSegments,
offset,
center: Vec.Add(innerBox.point, innerBox.size),
radius,
startAngle: 0,
endAngle: HALF_PI,
})
offset += numArcSegments * 6
// bottom left
pie(data, {
numArcSegments,
offset,
center: Vec.Add(innerBox.point, new Vec(0, innerBox.h)),
radius,
startAngle: HALF_PI,
endAngle: PI,
})
return roundedRectangleDataSize
}

Wyświetl plik

@ -20,6 +20,7 @@ import {
useEditor,
} from '@tldraw/editor'
import * as React from 'react'
import { ADJACENT_NOTE_MARGIN } from '../../shapes/note/noteHelpers'
import { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers'
import { getEmbedInfo } from '../../utils/embeds/embeds'
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
@ -818,7 +819,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
trackEvent('pack-shapes', { source })
editor.mark('pack')
const selectedShapeIds = editor.getSelectedShapeIds()
editor.packShapes(selectedShapeIds, 16)
editor.packShapes(selectedShapeIds, ADJACENT_NOTE_MARGIN)
kickoutOccludedShapes(editor, selectedShapeIds)
},
},
@ -1036,7 +1037,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true,
onSelect(source) {
trackEvent('zoom-in', { source })
editor.zoomIn(editor.getViewportScreenCenter(), {
editor.zoomIn(undefined, {
animation: { duration: ANIMATION_MEDIUM_MS },
})
},
@ -1048,7 +1049,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true,
onSelect(source) {
trackEvent('zoom-out', { source })
editor.zoomOut(editor.getViewportScreenCenter(), {
editor.zoomOut(undefined, {
animation: { duration: ANIMATION_MEDIUM_MS },
})
},
@ -1061,7 +1062,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true,
onSelect(source) {
trackEvent('reset-zoom', { source })
editor.resetZoom(editor.getViewportScreenCenter(), {
editor.resetZoom(undefined, {
animation: { duration: ANIMATION_MEDIUM_MS },
})
},
@ -1296,7 +1297,12 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true,
onSelect(source) {
trackEvent('zoom-to-content', { source })
editor.zoomToContent()
const bounds = editor.getSelectionPageBounds() ?? editor.getCurrentPageBounds()
if (!bounds) return
editor.zoomToBounds(bounds, {
targetZoom: Math.min(1, editor.getZoomLevel()),
animation: { duration: 220 },
})
},
},
{

Wyświetl plik

@ -1,4 +1,4 @@
import { createContext, useContext } from 'react'
import { createContext, useContext, useEffect } from 'react'
import { TLUiAssetUrls } from '../assetUrls'
/** @internal */
@ -14,6 +14,19 @@ export function AssetUrlsProvider({
assetUrls: TLUiAssetUrls
children: React.ReactNode
}) {
useEffect(() => {
for (const src of Object.values(assetUrls.icons)) {
const image = new Image()
image.src = src
image.decode()
}
for (const src of Object.values(assetUrls.embedIcons)) {
const image = new Image()
image.src = src
image.decode()
}
}, [assetUrls])
return <AssetUrlsContext.Provider value={assetUrls}>{children}</AssetUrlsContext.Provider>
}

Wyświetl plik

@ -9,6 +9,8 @@ import {
TLTextShape,
VecLike,
isNonNull,
preventDefault,
stopEventPropagation,
uniq,
useEditor,
useValue,
@ -615,24 +617,29 @@ export function useNativeClipboardEvents() {
useEffect(() => {
if (!appIsFocused) return
const copy = () => {
const copy = (e: ClipboardEvent) => {
if (
editor.getSelectedShapeIds().length === 0 ||
editor.getEditingShapeId() !== null ||
disallowClipboardEvents(editor)
)
) {
return
}
preventDefault(e)
handleNativeOrMenuCopy(editor)
trackEvent('copy', { source: 'kbd' })
}
function cut() {
function cut(e: ClipboardEvent) {
if (
editor.getSelectedShapeIds().length === 0 ||
editor.getEditingShapeId() !== null ||
disallowClipboardEvents(editor)
)
) {
return
}
preventDefault(e)
handleNativeOrMenuCopy(editor)
editor.deleteShapes(editor.getSelectedShapeIds())
trackEvent('cut', { source: 'kbd' })
@ -641,6 +648,7 @@ export function useNativeClipboardEvents() {
let disablingMiddleClickPaste = false
const pointerUpHandler = (e: PointerEvent) => {
if (e.button === 1) {
// middle mouse button
disablingMiddleClickPaste = true
requestAnimationFrame(() => {
disablingMiddleClickPaste = false
@ -648,9 +656,9 @@ export function useNativeClipboardEvents() {
}
}
const paste = (event: ClipboardEvent) => {
const paste = (e: ClipboardEvent) => {
if (disablingMiddleClickPaste) {
event.stopPropagation()
stopEventPropagation(e)
return
}
@ -660,8 +668,8 @@ export function useNativeClipboardEvents() {
if (editor.getEditingShapeId() !== null || disallowClipboardEvents(editor)) return
// First try to use the clipboard data on the event
if (event.clipboardData && !editor.inputs.shiftKey) {
handlePasteFromEventClipboardData(editor, event.clipboardData)
if (e.clipboardData && !editor.inputs.shiftKey) {
handlePasteFromEventClipboardData(editor, e.clipboardData)
} else {
// Or else use the clipboard API
navigator.clipboard.read().then((clipboardItems) => {
@ -671,6 +679,7 @@ export function useNativeClipboardEvents() {
})
}
preventDefault(e)
trackEvent('paste', { source: 'kbd' })
}

Wyświetl plik

@ -1,38 +0,0 @@
import { useEffect, useState } from 'react'
import { useAssetUrls } from '../context/asset-urls'
import { iconTypes } from '../icon-types'
/** @internal */
export function usePreloadIcons(): boolean {
const [isLoaded, setIsLoaded] = useState<boolean>(false)
const assetUrls = useAssetUrls()
useEffect(() => {
let cancelled = false
async function loadImages() {
// Run through all of the icons and load them. It doesn't matter
// if any of the images don't load; though we expect that they would.
// Instead, we just want to make sure that the browser has cached
// all of the icons it can so that they're available when we need them.
await Promise.allSettled(
iconTypes.map((icon) => {
const image = new Image()
image.src = assetUrls.icons[icon]
return image.decode()
})
)
if (cancelled) return
setIsLoaded(true)
}
loadImages()
return () => {
cancelled = true
}
}, [isLoaded, assetUrls])
return isLoaded
}

Wyświetl plik

@ -62,7 +62,7 @@ const schemaV2 = T.object<SerializedSchemaV2>({
const tldrawFileValidator: T.Validator<TldrawFile> = T.object({
tldrawFileFormatVersion: T.nonZeroInteger,
schema: T.union('schemaVersion', {
schema: T.numberUnion('schemaVersion', {
1: schemaV1,
2: schemaV2,
}),
@ -305,7 +305,6 @@ export async function parseAndLoadDocument(
editor.history.clear()
// Put the old bounds back in place
editor.updateViewportScreenBounds(initialBounds)
editor.updateRenderingBounds()
const bounds = editor.getCurrentPageBounds()
if (bounds) {

Wyświetl plik

@ -22,6 +22,7 @@ import {
TLWheelEventInfo,
Vec,
VecLike,
computed,
createShapeId,
createTLStore,
rotateSelectionHandle,
@ -143,6 +144,15 @@ export class TestEditor extends Editor {
elm: HTMLDivElement
bounds = { x: 0, y: 0, top: 0, left: 0, width: 1080, height: 720, bottom: 720, right: 1080 }
/**
* The center of the viewport in the current page space.
*
* @public
*/
@computed getViewportPageCenter() {
return this.getViewportPageBounds().center
}
setScreenBounds(bounds: BoxModel, center = false) {
this.bounds.x = bounds.x
this.bounds.y = bounds.y
@ -154,7 +164,6 @@ export class TestEditor extends Editor {
this.bounds.bottom = bounds.y + bounds.h
this.updateViewportScreenBounds(Box.From(bounds), center)
this.updateRenderingBounds()
return this
}
@ -200,12 +209,12 @@ export class TestEditor extends Editor {
* _transformPointerDownSpy.mockRestore())
*/
_transformPointerDownSpy = jest
.spyOn(this._clickManager, 'transformPointerDownEvent')
.spyOn(this._clickManager, 'handlePointerEvent')
.mockImplementation((info) => {
return info
})
_transformPointerUpSpy = jest
.spyOn(this._clickManager, 'transformPointerDownEvent')
.spyOn(this._clickManager, 'handlePointerEvent')
.mockImplementation((info) => {
return info
})
@ -455,6 +464,16 @@ export class TestEditor extends Editor {
return this
}
pan(offset: VecLike): this {
const { isLocked, panSpeed } = this.getCameraOptions()
if (isLocked) return this
const { x: cx, y: cy, z: cz } = this.getCamera()
this.setCamera(new Vec(cx + (offset.x * panSpeed) / cz, cy + (offset.y * panSpeed) / cz, cz), {
immediate: true,
})
return this
}
pinchStart = (
x = this.inputs.currentScreenPoint.x,
y = this.inputs.currentScreenPoint.y,

Wyświetl plik

@ -0,0 +1,89 @@
import { TLCameraOptions } from '@tldraw/editor'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
})
describe('getBaseZoom', () => {
it('gets initial zoom with default options', () => {
expect(editor.getBaseZoom()).toBe(1)
})
it('gets initial zoom based on constraints', () => {
const vsb = editor.getViewportScreenBounds()
let cameraOptions: TLCameraOptions
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
padding: { x: 0, y: 0 },
origin: { x: 0.5, y: 0.5 },
initialZoom: 'default',
baseZoom: 'default',
behavior: 'free',
},
})
expect(editor.getBaseZoom()).toBe(1)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
baseZoom: 'fit-x',
},
})
expect(editor.getBaseZoom()).toBe(0.5)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
baseZoom: 'fit-y',
},
})
expect(editor.getBaseZoom()).toBe(0.25)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
baseZoom: 'fit-min',
},
})
expect(editor.getBaseZoom()).toBe(0.5)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
baseZoom: 'fit-max',
},
})
expect(editor.getBaseZoom()).toBe(0.25)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
baseZoom: 'default',
},
})
expect(editor.getBaseZoom()).toBe(1)
})
})

Wyświetl plik

@ -0,0 +1,89 @@
import { TLCameraOptions } from '@tldraw/editor'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
})
describe('getInitialZoom', () => {
it('gets initial zoom with default options', () => {
expect(editor.getInitialZoom()).toBe(1)
})
it('gets initial zoom based on constraints', () => {
const vsb = editor.getViewportScreenBounds()
let cameraOptions: TLCameraOptions
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
padding: { x: 0, y: 0 },
origin: { x: 0.5, y: 0.5 },
initialZoom: 'default',
baseZoom: 'default',
behavior: 'free',
},
})
expect(editor.getInitialZoom()).toBe(1)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
initialZoom: 'fit-x',
},
})
expect(editor.getInitialZoom()).toBe(0.5)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
initialZoom: 'fit-y',
},
})
expect(editor.getInitialZoom()).toBe(0.25)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
initialZoom: 'fit-min',
},
})
expect(editor.getInitialZoom()).toBe(0.5)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
initialZoom: 'fit-max',
},
})
expect(editor.getInitialZoom()).toBe(0.25)
cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
...(cameraOptions.constraints as any),
initialZoom: 'default',
},
})
expect(editor.getInitialZoom()).toBe(1)
})
})

Wyświetl plik

@ -1,4 +1,4 @@
import { getDefaultCameraOptions } from '@tldraw/editor'
import { DEFAULT_CAMERA_OPTIONS } from '@tldraw/editor'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
@ -8,7 +8,7 @@ beforeEach(() => {
})
it('zooms by increments', () => {
const cameraOptions = getDefaultCameraOptions()
const cameraOptions = DEFAULT_CAMERA_OPTIONS
// Starts at 1
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
@ -46,7 +46,7 @@ it('preserves the screen center when offset', () => {
})
it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => {
const cameraOptions = getDefaultCameraOptions()
const cameraOptions = DEFAULT_CAMERA_OPTIONS
editor.setCamera({ x: 0, y: 0, z: (cameraOptions.zoomSteps[2] + cameraOptions.zoomSteps[3]) / 2 })
editor.zoomIn()

Wyświetl plik

@ -1,4 +1,3 @@
import { getDefaultCameraOptions } from '@tldraw/editor'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
@ -8,7 +7,7 @@ beforeEach(() => {
})
it('zooms out and in by increments', () => {
const cameraOptions = getDefaultCameraOptions()
const cameraOptions = editor.getCameraOptions()
// Starts at 1
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
@ -30,3 +29,87 @@ it('does not zoom out when camera is frozen', () => {
editor.zoomOut()
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
})
it('zooms out and in by increments when the camera options have constraints but no base zoom', () => {
const cameraOptions = editor.getCameraOptions()
editor.setCameraOptions({
...cameraOptions,
constraints: {
bounds: { x: 0, y: 0, w: 1600, h: 900 },
padding: { x: 0, y: 0 },
origin: { x: 0.5, y: 0.5 },
initialZoom: 'default',
baseZoom: 'default',
behavior: 'free',
},
})
// Starts at 1
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[2])
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[1])
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0])
// does not zoom out past min
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0])
})
it('zooms out and in by increments when the camera options have constraints and a base zoom', () => {
const cameraOptions = editor.getCameraOptions()
const vsb = editor.getViewportScreenBounds()
editor.setCameraOptions({
...cameraOptions,
constraints: {
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
padding: { x: 0, y: 0 },
origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-x',
baseZoom: 'fit-x',
behavior: 'free',
},
})
// And reset the zoom to its initial value
editor.resetZoom()
expect(editor.getInitialZoom()).toBe(0.5) // fitting the x axis
// Starts at 1
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3] * 0.5)
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[2] * 0.5)
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[1] * 0.5)
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.5)
// does not zoom out past min
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.5)
editor.setCameraOptions({
...cameraOptions,
constraints: {
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
padding: { x: 0, y: 0 },
origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-y',
baseZoom: 'fit-y',
behavior: 'free',
},
})
// And reset the zoom to its initial value
editor.resetZoom()
expect(editor.getInitialZoom()).toBe(0.25) // fitting the y axis
// Starts at 1
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3] * 0.25)
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[2] * 0.25)
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[1] * 0.25)
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.25)
// does not zoom out past min
editor.zoomOut()
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.25)
})

Wyświetl plik

@ -1,5 +1,6 @@
import { TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
import { TestEditor } from './TestEditor'
import { TEST_DRAW_SHAPE_SCREEN_POINTS } from './drawing.data'
jest.useFakeTimers()
@ -260,3 +261,22 @@ for (const toolType of ['draw', 'highlight'] as const) {
})
})
}
it('Draws a bunch', () => {
editor.setCurrentTool('draw').setCamera({ x: 0, y: 0, z: 1 })
const [first, ...rest] = TEST_DRAW_SHAPE_SCREEN_POINTS
editor.pointerMove(first.x, first.y).pointerDown()
for (const point of rest) {
editor.pointerMove(point.x, point.y)
}
editor.pointerUp()
editor.selectAll()
const shape = { ...editor.getLastCreatedShape() }
// @ts-expect-error
delete shape.id
expect(shape).toMatchSnapshot('draw shape')
})

Wyświetl plik

@ -7,7 +7,6 @@ let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
editor.renderingBoundsMargin = 100
})
function createShapes() {
@ -136,3 +135,56 @@ it('correctly calculates the culled shapes when adding and deleting shapes', ()
const culledShapeFromScratch = editor.getCulledShapes()
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)
})
it('works for shapes that are outside of the viewport, but are then moved inside it', () => {
const box1Id = createShapeId()
const box2Id = createShapeId()
const arrowId = createShapeId()
editor.createShapes([
{
id: box1Id,
props: { w: 100, h: 100, geo: 'rectangle' },
type: 'geo',
x: -500,
y: 0,
},
{
id: box2Id,
type: 'geo',
x: -1000,
y: 200,
props: { w: 100, h: 100, geo: 'rectangle' },
},
{
id: arrowId,
type: 'arrow',
props: {
start: {
type: 'binding',
isExact: true,
boundShapeId: box1Id,
normalizedAnchor: { x: 0.5, y: 0.5 },
isPrecise: false,
},
end: {
type: 'binding',
isExact: true,
boundShapeId: box2Id,
normalizedAnchor: { x: 0.5, y: 0.5 },
isPrecise: false,
},
},
},
])
expect(editor.getCulledShapes()).toEqual(new Set([box1Id, box2Id, arrowId]))
// Move box1 and box2 inside the viewport
editor.updateShapes([
{ id: box1Id, type: 'geo', x: 100 },
{ id: box2Id, type: 'geo', x: 200 },
])
// Arrow should also not be culled
expect(editor.getCulledShapes()).toEqual(new Set())
})

Wyświetl plik

@ -419,7 +419,6 @@ describe('When pasting into frames...', () => {
.bringToFront(editor.getSelectedShapeIds())
editor.setCamera({ x: -2000, y: -2000, z: 1 })
editor.updateRenderingBounds()
// Copy box 1 (should be out of viewport)
editor.select(ids.box1).copy()

Wyświetl plik

@ -34,7 +34,6 @@ function normalizeIndexes(
beforeEach(() => {
editor = new TestEditor()
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
editor.renderingBoundsMargin = 100
})
function createShapes() {
@ -48,18 +47,6 @@ function createShapes() {
])
}
it('updates the rendering viewport when the camera stops moving', () => {
const ids = createShapes()
editor.updateRenderingBounds = jest.fn(editor.updateRenderingBounds)
editor.pan({ x: -201, y: -201 })
jest.advanceTimersByTime(500)
expect(editor.updateRenderingBounds).toHaveBeenCalledTimes(1)
expect(editor.getRenderingBounds()).toMatchObject({ x: 201, y: 201, w: 1800, h: 900 })
expect(editor.getShapePageBounds(ids.A)).toMatchObject({ x: 100, y: 100, w: 100, h: 100 })
})
it('lists shapes in viewport sorted by id with correct indexes & background indexes', () => {
const ids = createShapes()
// Expect the results to be sorted correctly by id

Wyświetl plik

@ -34,15 +34,17 @@ export function measureAverageDuration(
const start = performance.now()
const result = originalMethod.apply(this, args)
const end = performance.now()
const value = averages.get(descriptor.value)!
const length = end - start
const total = value.total + length
const count = value.count + 1
averages.set(descriptor.value, { total, count })
// eslint-disable-next-line no-console
console.log(
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
)
if (length !== 0) {
const value = averages.get(descriptor.value)!
const total = value.total + length
const count = value.count + 1
averages.set(descriptor.value, { total, count })
// eslint-disable-next-line no-console
console.log(
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
)
}
return result
}
averages.set(descriptor.value, { total: 0, count: 0 })

Wyświetl plik

@ -83,6 +83,9 @@ function nullable<T>(validator: Validatable<T>): Validator<null | T>;
// @public
const number: Validator<number>;
// @internal (undocumented)
function numberUnion<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(key: Key, config: Config): UnionValidator<Key, Config>;
// @public
function object<Shape extends object>(config: {
readonly [K in keyof Shape]: Validatable<Shape[K]>;
@ -134,6 +137,7 @@ declare namespace T {
jsonDict,
dict,
union,
numberUnion,
model,
setEnum,
optional,
@ -178,7 +182,7 @@ function union<Key extends string, Config extends UnionValidatorConfig<Key, Conf
// @public (undocumented)
export class UnionValidator<Key extends string, Config extends UnionValidatorConfig<Key, Config>, UnknownValue = never> extends Validator<TypeOf<Config[keyof Config]> | UnknownValue> {
constructor(key: Key, config: Config, unknownValueValidation: (value: object, variant: string) => UnknownValue);
constructor(key: Key, config: Config, unknownValueValidation: (value: object, variant: string) => UnknownValue, useNumberKeys: boolean);
// (undocumented)
validateUnknownVariants<Unknown>(unknownValueValidation: (value: object, variant: string) => Unknown): UnionValidator<Key, Config, Unknown>;
}

Wyświetl plik

@ -3027,6 +3027,14 @@
"kind": "Content",
"text": "(value: object, variant: string) => UnknownValue"
},
{
"kind": "Content",
"text": ", useNumberKeys: "
},
{
"kind": "Content",
"text": "boolean"
},
{
"kind": "Content",
"text": ");"
@ -3059,6 +3067,14 @@
"endIndex": 6
},
"isOptional": false
},
{
"parameterName": "useNumberKeys",
"parameterTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"isOptional": false
}
]
},
@ -4260,6 +4276,14 @@
"kind": "Content",
"text": "(value: object, variant: string) => UnknownValue"
},
{
"kind": "Content",
"text": ", useNumberKeys: "
},
{
"kind": "Content",
"text": "boolean"
},
{
"kind": "Content",
"text": ");"
@ -4292,6 +4316,14 @@
"endIndex": 6
},
"isOptional": false
},
{
"parameterName": "useNumberKeys",
"parameterTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"isOptional": false
}
]
},

Wyświetl plik

@ -394,7 +394,8 @@ export class UnionValidator<
constructor(
private readonly key: Key,
private readonly config: Config,
private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue
private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue,
private readonly useNumberKeys: boolean
) {
super(
(input) => {
@ -442,11 +443,13 @@ export class UnionValidator<
matchingSchema: Validatable<any> | undefined
variant: string
} {
const variant = getOwnProperty(object, this.key) as keyof Config | undefined
if (typeof variant !== 'string') {
const variant = getOwnProperty(object, this.key) as string & keyof Config
if (!this.useNumberKeys && typeof variant !== 'string') {
throw new ValidationError(
`Expected a string for key "${this.key}", got ${typeToString(variant)}`
)
} else if (this.useNumberKeys && !Number.isFinite(Number(variant))) {
throw new ValidationError(`Expected a number for key "${this.key}", got "${variant as any}"`)
}
const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined
@ -456,7 +459,7 @@ export class UnionValidator<
validateUnknownVariants<Unknown>(
unknownValueValidation: (value: object, variant: string) => Unknown
): UnionValidator<Key, Config, Unknown> {
return new UnionValidator(this.key, this.config, unknownValueValidation)
return new UnionValidator(this.key, this.config, unknownValueValidation, this.useNumberKeys)
}
}
@ -829,14 +832,41 @@ export function union<Key extends string, Config extends UnionValidatorConfig<Ke
key: Key,
config: Config
): UnionValidator<Key, Config> {
return new UnionValidator(key, config, (unknownValue, unknownVariant) => {
throw new ValidationError(
`Expected one of ${Object.keys(config)
.map((key) => JSON.stringify(key))
.join(' or ')}, got ${JSON.stringify(unknownVariant)}`,
[key]
)
})
return new UnionValidator(
key,
config,
(unknownValue, unknownVariant) => {
throw new ValidationError(
`Expected one of ${Object.keys(config)
.map((key) => JSON.stringify(key))
.join(' or ')}, got ${JSON.stringify(unknownVariant)}`,
[key]
)
},
false
)
}
/**
* @internal
*/
export function numberUnion<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(
key: Key,
config: Config
): UnionValidator<Key, Config> {
return new UnionValidator(
key,
config,
(unknownValue, unknownVariant) => {
throw new ValidationError(
`Expected one of ${Object.keys(config)
.map((key) => JSON.stringify(key))
.join(' or ')}, got ${JSON.stringify(unknownVariant)}`,
[key]
)
},
true
)
}
/**

Wyświetl plik

@ -515,7 +515,7 @@ async function coalesceWithPreviousAssets(assetsDir: string) {
// and it will mess up the inline source viewer on sentry errors.
const out = tar.x({ cwd: assetsDir, 'keep-existing': true })
for await (const chunk of Body?.transformToWebStream() as any as AsyncIterable<Uint8Array>) {
out.write(chunk)
out.write(Buffer.from(chunk.buffer))
}
out.end()
}

Wyświetl plik

@ -18,12 +18,12 @@ async function hasPackageChanged(pkg: PackageDetails) {
}
const publishedTarballPath = `${dirPath}/published-package.tgz`
writeFileSync(publishedTarballPath, Buffer.from(await res.arrayBuffer()))
const publishedManifest = await getTarballManifest(publishedTarballPath)
const publishedManifest = getTarballManifestSync(publishedTarballPath)
const localTarballPath = `${dirPath}/local-package.tgz`
await exec('yarn', ['pack', '--out', localTarballPath], { pwd: pkg.dir })
const localManifest = await getTarballManifest(localTarballPath)
const localManifest = getTarballManifestSync(localTarballPath)
return !manifestsAreEqual(publishedManifest, localManifest)
} finally {
@ -48,34 +48,25 @@ function manifestsAreEqual(a: Record<string, Buffer>, b: Record<string, Buffer>)
return true
}
function getTarballManifest(tarballPath: string): Promise<Record<string, Buffer>> {
function getTarballManifestSync(tarballPath: string) {
const manifest: Record<string, Buffer> = {}
return new Promise((resolve, reject) =>
tar.list(
{
// @ts-expect-error bad typings
file: tarballPath,
onentry: (entry) => {
entry.on('data', (data) => {
// we could hash these to reduce memory but it's probably fine
const existing = manifest[entry.path]
if (existing) {
manifest[entry.path] = Buffer.concat([existing, data])
} else {
manifest[entry.path] = data
}
})
},
},
(err: any) => {
if (err) {
reject(err)
tar.list({
file: tarballPath,
onentry: (entry) => {
entry.on('data', (data) => {
// we could hash these to reduce memory but it's probably fine
const existing = manifest[entry.path]
if (existing) {
manifest[entry.path] = Buffer.concat([existing, data])
} else {
resolve(manifest)
manifest[entry.path] = data
}
}
)
)
})
},
sync: true,
})
return manifest
}
export async function didAnyPackageChange() {

Wyświetl plik

@ -59,7 +59,7 @@
"@types/tmp": "^0.2.6",
"ignore": "^5.2.4",
"minimist": "^1.2.8",
"tar": "^6.2.0",
"tar": "^7.0.1",
"tmp": "^0.2.3"
}
}

113
yarn.lock
Wyświetl plik

@ -3680,6 +3680,15 @@ __metadata:
languageName: node
linkType: hard
"@isaacs/fs-minipass@npm:^4.0.0":
version: 4.0.0
resolution: "@isaacs/fs-minipass@npm:4.0.0"
dependencies:
minipass: "npm:^7.0.4"
checksum: 7444d7a3c9211c27494630e2bff8545e3494a1598624a4871ee7ef3a9e592a61fed3abd85d118f966673bd0b4401c266d45441f89c00c420e9d0cfbf1042dbd5
languageName: node
linkType: hard
"@istanbuljs/load-nyc-config@npm:^1.0.0":
version: 1.1.0
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
@ -7589,7 +7598,7 @@ __metadata:
rimraf: "npm:^4.4.0"
semver: "npm:^7.3.8"
svgo: "npm:^3.0.2"
tar: "npm:^6.2.0"
tar: "npm:^7.0.1"
tmp: "npm:^0.2.3"
typescript: "npm:^5.3.3"
languageName: unknown
@ -10700,6 +10709,13 @@ __metadata:
languageName: node
linkType: hard
"chownr@npm:^3.0.0":
version: 3.0.0
resolution: "chownr@npm:3.0.0"
checksum: b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c
languageName: node
linkType: hard
"chrome-trace-event@npm:^1.0.2":
version: 1.0.3
resolution: "chrome-trace-event@npm:1.0.3"
@ -14645,18 +14661,18 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^10.2.2, glob@npm:^10.3.10":
version: 10.3.10
resolution: "glob@npm:10.3.10"
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7":
version: 10.3.12
resolution: "glob@npm:10.3.12"
dependencies:
foreground-child: "npm:^3.1.0"
jackspeak: "npm:^2.3.5"
jackspeak: "npm:^2.3.6"
minimatch: "npm:^9.0.1"
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
path-scurry: "npm:^1.10.1"
minipass: "npm:^7.0.4"
path-scurry: "npm:^1.10.2"
bin:
glob: dist/esm/bin.mjs
checksum: 38bdb2c9ce75eb5ed168f309d4ed05b0798f640b637034800a6bf306f39d35409bf278b0eaaffaec07591085d3acb7184a201eae791468f0f617771c2486a6a8
checksum: 9e8186abc22dc824b5dd86cefd8e6b5621a72d1be7f68bacc0fd681e8c162ec5546660a6ec0553d6a74757a585e655956c7f8f1a6d24570e8d865c307323d178
languageName: node
linkType: hard
@ -16275,7 +16291,7 @@ __metadata:
languageName: node
linkType: hard
"jackspeak@npm:^2.3.5":
"jackspeak@npm:^2.3.6":
version: 2.3.6
resolution: "jackspeak@npm:2.3.6"
dependencies:
@ -17721,10 +17737,10 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0":
version: 10.1.0
resolution: "lru-cache@npm:10.1.0"
checksum: 207278d6fa711fb1f94a0835d4d4737441d2475302482a14785b10515e4c906a57ebf9f35bf060740c9560e91c7c1ad5a04fd7ed030972a9ba18bce2a228e95b
"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
version: 10.2.0
resolution: "lru-cache@npm:10.2.0"
checksum: 502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302
languageName: node
linkType: hard
@ -19131,7 +19147,7 @@ __metadata:
languageName: node
linkType: hard
"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3":
"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4":
version: 7.0.4
resolution: "minipass@npm:7.0.4"
checksum: e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18
@ -19148,6 +19164,16 @@ __metadata:
languageName: node
linkType: hard
"minizlib@npm:^3.0.1":
version: 3.0.1
resolution: "minizlib@npm:3.0.1"
dependencies:
minipass: "npm:^7.0.4"
rimraf: "npm:^5.0.5"
checksum: 622cb85f51e5c206a080a62d20db0d7b4066f308cb6ce82a9644da112367c3416ae7062017e631eb7ac8588191cfa4a9a279b8651c399265202b298e98c4acef
languageName: node
linkType: hard
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
version: 0.5.3
resolution: "mkdirp-classic@npm:0.5.3"
@ -19164,6 +19190,15 @@ __metadata:
languageName: node
linkType: hard
"mkdirp@npm:^3.0.1":
version: 3.0.1
resolution: "mkdirp@npm:3.0.1"
bin:
mkdirp: dist/cjs/src/bin.js
checksum: 16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba
languageName: node
linkType: hard
"mlly@npm:^1.1.0, mlly@npm:^1.2.0":
version: 1.5.0
resolution: "mlly@npm:1.5.0"
@ -20327,13 +20362,13 @@ __metadata:
languageName: node
linkType: hard
"path-scurry@npm:^1.10.1, path-scurry@npm:^1.6.1":
version: 1.10.1
resolution: "path-scurry@npm:1.10.1"
"path-scurry@npm:^1.10.2, path-scurry@npm:^1.6.1":
version: 1.10.2
resolution: "path-scurry@npm:1.10.2"
dependencies:
lru-cache: "npm:^9.1.1 || ^10.0.0"
lru-cache: "npm:^10.2.0"
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
checksum: eebfb8304fef1d4f7e1486df987e4fd77413de4fce16508dea69fcf8eb318c09a6b15a7a2f4c22877cec1cb7ecbd3071d18ca9de79eeece0df874a00f1f0bdc8
checksum: a2bbbe8dc284c49dd9be78ca25f3a8b89300e0acc24a77e6c74824d353ef50efbf163e64a69f4330b301afca42d0e2229be0560d6d616ac4e99d48b4062016b1
languageName: node
linkType: hard
@ -22045,6 +22080,17 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^5.0.5":
version: 5.0.5
resolution: "rimraf@npm:5.0.5"
dependencies:
glob: "npm:^10.3.7"
bin:
rimraf: dist/esm/bin.mjs
checksum: a612c7184f96258b7d1328c486b12ca7b60aa30e04229a08bbfa7e964486deb1e9a1b52d917809311bdc39a808a4055c0f950c0280fba194ba0a09e6f0d404f6
languageName: node
linkType: hard
"rollup-plugin-inject@npm:^3.0.0":
version: 3.0.2
resolution: "rollup-plugin-inject@npm:3.0.2"
@ -23378,7 +23424,7 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2, tar@npm:^6.2.0":
"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2":
version: 6.2.1
resolution: "tar@npm:6.2.1"
dependencies:
@ -23392,6 +23438,20 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:^7.0.1":
version: 7.0.1
resolution: "tar@npm:7.0.1"
dependencies:
"@isaacs/fs-minipass": "npm:^4.0.0"
chownr: "npm:^3.0.0"
minipass: "npm:^5.0.0"
minizlib: "npm:^3.0.1"
mkdirp: "npm:^3.0.1"
yallist: "npm:^5.0.0"
checksum: 6fd89ef8051d12975f66a2f3932a80479bdc6c9f3bcdf04b8b57784e942ed860708ccecf79bcbb30659b14ab52eef2095d2c3af377545ff9df30de28036671dc
languageName: node
linkType: hard
"terminal-link@npm:^2.1.1":
version: 2.1.1
resolution: "terminal-link@npm:2.1.1"
@ -24964,8 +25024,8 @@ __metadata:
linkType: hard
"vite@npm:^5.0.0":
version: 5.2.8
resolution: "vite@npm:5.2.8"
version: 5.2.9
resolution: "vite@npm:5.2.9"
dependencies:
esbuild: "npm:^0.20.1"
fsevents: "npm:~2.3.3"
@ -24999,7 +25059,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: caa40343c2c4e6d8e257fccb4c3029f62909c319a86063ce727ed550925c0a834460b0d1ca20c4d6c915f35302aa1052f6ec5193099a47ce21d74b9b817e69e1
checksum: 26342c8dde540e4161fdad2c9c8f2f0e23567f051c7a40abb8e4796d6c4292fbd118ab7a4ac252515e78c4f99525b557731e6117287b2bccde0ea61d73bcff27
languageName: node
linkType: hard
@ -25666,6 +25726,13 @@ __metadata:
languageName: node
linkType: hard
"yallist@npm:^5.0.0":
version: 5.0.0
resolution: "yallist@npm:5.0.0"
checksum: 1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a
languageName: node
linkType: hard
"yaml@npm:2.3.4, yaml@npm:^2.0.0, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.4":
version: 2.3.4
resolution: "yaml@npm:2.3.4"