Porównaj commity

...

11 Commity

Autor SHA1 Wiadomość Data
Mitja Bezenšek 4245fd55b2
Fix deploy script (#3550)
Seems like `tar` is moving to `ts` in version 7 and this caused some
issues with imports.

Saw this issue on [readonly
PR](https://github.com/tldraw/tldraw/actions/runs/8783569356/job/24099998235?pr=3192#step:6:684),
looks like a result of a [dependabot
PR](https://github.com/tldraw/tldraw/pull/3505).

### 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
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [x] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know
2024-04-22 15:33:25 +00:00
alex cce794e04b
Expose `usePreloadAssets` (#3545)
Expose `usePreloadAssets` and make sure the exploded/sublibraries
examples uses it. Before this change, fonts weren't loaded correctly for
the exploded example.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `docs` — Changes to the documentation, examples, or templates.
- [x] `bugfix` — Bug fix
2024-04-22 10:32:22 +00: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 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
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
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
33 zmienionych plików z 3354 dodań i 606 usunięć

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,6 +1,8 @@
import {
ContextMenu,
DefaultContextMenuContent,
ErrorScreen,
LoadingScreen,
TldrawEditor,
TldrawHandles,
TldrawScribble,
@ -10,7 +12,9 @@ import {
defaultShapeTools,
defaultShapeUtils,
defaultTools,
usePreloadAssets,
} from 'tldraw'
import { defaultEditorAssetUrls } from 'tldraw/src/lib/utils/static-assets/assetUrls'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
@ -26,6 +30,16 @@ const defaultComponents = {
//[2]
export default function ExplodedExample() {
const assetLoading = usePreloadAssets(defaultEditorAssetUrls)
if (assetLoading.error) {
return <ErrorScreen>Could not load assets.</ErrorScreen>
}
if (!assetLoading.done) {
return <LoadingScreen>Loading assets...</LoadingScreen>
}
return (
<div className="tldraw__editor">
<TldrawEditor

Wyświetl plik

@ -1,3 +1,7 @@
## 2.0.30
- Fixes a bug that prevented opening some files.
## 2.0.29
- Improved note shapes.

Wyświetl plik

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

Wyświetl plik

@ -682,6 +682,8 @@ export class Editor extends EventEmitter<TLEventMap> {
getCameraState(): "idle" | "moving";
getCanRedo(): boolean;
getCanUndo(): boolean;
getCollaborators(): TLInstancePresence[];
getCollaboratorsOnCurrentPage(): TLInstancePresence[];
getContainer: () => HTMLElement;
getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined;
// @internal
@ -693,6 +695,8 @@ export class Editor extends EventEmitter<TLEventMap> {
getCurrentPageId(): TLPageId;
getCurrentPageRenderingShapesSorted(): TLShape[];
getCurrentPageShapeIds(): Set<TLShapeId>;
// @internal (undocumented)
getCurrentPageShapeIdsSorted(): TLShapeId[];
getCurrentPageShapes(): TLShape[];
getCurrentPageShapesSorted(): TLShape[];
getCurrentPageState(): TLInstancePageState;

Wyświetl plik

@ -10059,6 +10059,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",

Wyświetl plik

@ -2619,15 +2619,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
animateToUser(userId: string): this {
const presences = this.store.query.records('instance_presence', () => ({
userId: { eq: userId },
}))
const presence = [...presences.get()]
.sort((a, b) => {
return a.lastActivityTimestamp - b.lastActivityTimestamp
})
.pop()
const presence = this.getCollaborators().find((c) => c.userId === userId)
if (!presence) return this
@ -2883,6 +2875,45 @@ export class Editor extends EventEmitter<TLEventMap> {
z: point.z ?? 0.5,
}
}
// Collaborators
@computed
private _getCollaboratorsQuery() {
return this.store.query.records('instance_presence', () => ({
userId: { neq: this.user.getId() },
}))
}
/**
* Returns a list of presence records for all peer collaborators.
* This will return the latest presence record for each connected user.
*
* @public
*/
@computed
getCollaborators() {
const allPresenceRecords = this._getCollaboratorsQuery().get()
if (!allPresenceRecords.length) return EMPTY_ARRAY
const userIds = [...new Set(allPresenceRecords.map((c) => c.userId))].sort()
return userIds.map((id) => {
const latestPresence = allPresenceRecords
.filter((c) => c.userId === id)
.sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0]
return latestPresence
})
}
/**
* 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.
*
* @public
*/
@computed
getCollaboratorsOnCurrentPage() {
const currentPageId = this.getCurrentPageId()
return this.getCollaborators().filter((c) => c.currentPageId === currentPageId)
}
// Following
@ -2894,9 +2925,9 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
startFollowingUser(userId: string): this {
const leaderPresences = this.store.query.records('instance_presence', () => ({
userId: { eq: userId },
}))
const leaderPresences = this._getCollaboratorsQuery()
.get()
.filter((p) => p.userId === userId)
const thisUserId = this.user.getId()
@ -2905,7 +2936,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
// If the leader is following us, then we can't follow them
if (leaderPresences.get().some((p) => p.followingUserId === thisUserId)) {
if (leaderPresences.some((p) => p.followingUserId === thisUserId)) {
return this
}
@ -2924,7 +2955,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const moveTowardsUser = () => {
// Stop following if we can't find the user
const leaderPresence = [...leaderPresences.get()]
const leaderPresence = [...leaderPresences]
.sort((a, b) => {
return a.lastActivityTimestamp - b.lastActivityTimestamp
})
@ -3281,6 +3312,14 @@ export class Editor extends EventEmitter<TLEventMap> {
return this._currentPageShapeIds.get()
}
/**
* @internal
*/
@computed
getCurrentPageShapeIdsSorted() {
return Array.from(this.getCurrentPageShapeIds()).sort()
}
/**
* Get the ids of shapes on a page.
*
@ -3893,7 +3932,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
getShapePageTransform(shape: TLShape | TLShapeId): Mat {
const id = typeof shape === 'string' ? shape : this.getShape(shape)!.id
const id = typeof shape === 'string' ? shape : shape.id
return this._getShapePageTransformCache().get(id) ?? Mat.Identity()
}
@ -4227,7 +4266,7 @@ export class Editor extends EventEmitter<TLEventMap> {
@computed getCurrentPageBounds(): Box | undefined {
let commonBounds: Box | undefined
this.getCurrentPageShapeIds().forEach((shapeId) => {
this.getCurrentPageShapeIdsSorted().forEach((shapeId) => {
const bounds = this.getShapeMaskedPageBounds(shapeId)
if (!bounds) return
if (!commonBounds) {
@ -4556,28 +4595,11 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
@computed getCurrentPageShapesSorted(): TLShape[] {
const shapes = this.getCurrentPageShapes().sort(sortByIndex)
const parentChildMap = new Map<TLShapeId, TLShape[]>()
const result: TLShape[] = []
const topLevelShapes: TLShape[] = []
let shape: TLShape, parent: TLShape | undefined
for (let i = 0, n = shapes.length; i < n; i++) {
shape = shapes[i]
parent = this.getShape(shape.parentId)
if (parent) {
if (!parentChildMap.has(parent.id)) {
parentChildMap.set(parent.id, [])
}
parentChildMap.get(parent.id)!.push(shape)
} else {
// undefined if parent is a shape
topLevelShapes.push(shape)
}
}
const topLevelShapes = this.getSortedChildIdsForParent(this.getCurrentPageId())
for (let i = 0, n = topLevelShapes.length; i < n; i++) {
pushShapeWithDescendants(topLevelShapes[i], parentChildMap, result)
pushShapeWithDescendants(this, topLevelShapes[i], result)
}
return result
@ -8176,7 +8198,11 @@ export class Editor extends EventEmitter<TLEventMap> {
// it will be 0,0 when its actual screen position is equal
// to screenBounds.point. This is confusing!
currentScreenPoint.set(sx, sy)
currentPagePoint.set(sx / cz - cx, sy / cz - cy, sz)
const nx = sx / cz - cx
const ny = sy / cz - cy
if (isFinite(nx) && isFinite(ny)) {
currentPagePoint.set(nx, ny, sz)
}
this.inputs.isPen = info.type === 'pointer' && info.isPen
@ -8875,16 +8901,12 @@ function applyPartialToShape<T extends TLShape>(prev: T, partial?: TLShapePartia
return next
}
function pushShapeWithDescendants(
shape: TLShape,
parentChildMap: Map<TLShapeId, TLShape[]>,
result: TLShape[]
): void {
function pushShapeWithDescendants(editor: Editor, id: TLShapeId, result: TLShape[]): void {
const shape = editor.getShape(id)
if (!shape) return
result.push(shape)
const children = parentChildMap.get(shape.id)
if (children) {
for (let i = 0, n = children.length; i < n; i++) {
pushShapeWithDescendants(children[i], parentChildMap, result)
}
const childIds = editor.getSortedChildIdsForParent(id)
for (let i = 0, n = childIds.length; i < n; i++) {
pushShapeWithDescendants(editor, childIds[i], result)
}
}

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

@ -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

@ -2545,6 +2545,12 @@ export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void): reado
// @public (undocumented)
export function useNativeClipboardEvents(): void;
// @public (undocumented)
export function usePreloadAssets(assetUrls: TLEditorAssetUrls): {
done: boolean;
error: boolean;
};
// @public (undocumented)
export function useReadonly(): boolean;

Wyświetl plik

@ -27982,6 +27982,52 @@
"parameters": [],
"name": "useNativeClipboardEvents"
},
{
"kind": "Function",
"canonicalReference": "tldraw!usePreloadAssets:function(1)",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function usePreloadAssets(assetUrls: "
},
{
"kind": "Reference",
"text": "TLEditorAssetUrls",
"canonicalReference": "tldraw!~TLEditorAssetUrls:type"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "{\n done: boolean;\n error: boolean;\n}"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/tldraw/src/lib/ui/hooks/usePreloadAssets.ts",
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "assetUrls",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"name": "usePreloadAssets"
},
{
"kind": "Function",
"canonicalReference": "tldraw!useReadonly:function(1)",

Wyświetl plik

@ -87,6 +87,7 @@ export { useExportAs } from './lib/ui/hooks/useExportAs'
export { useKeyboardShortcuts } from './lib/ui/hooks/useKeyboardShortcuts'
export { useLocalStorageState } from './lib/ui/hooks/useLocalStorageState'
export { useMenuIsOpen } from './lib/ui/hooks/useMenuIsOpen'
export { usePreloadAssets } from './lib/ui/hooks/usePreloadAssets'
export { useReadonly } from './lib/ui/hooks/useReadonly'
export { useRelevantStyles } from './lib/ui/hooks/useRelevantStyles'
export {

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

@ -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

@ -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, { 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, { 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

@ -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' })
@ -648,9 +655,9 @@ export function useNativeClipboardEvents() {
}
}
const paste = (event: ClipboardEvent) => {
const paste = (e: ClipboardEvent) => {
if (disablingMiddleClickPaste) {
event.stopPropagation()
stopEventPropagation(e)
return
}
@ -660,8 +667,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 +678,7 @@ export function useNativeClipboardEvents() {
})
}
preventDefault(e)
trackEvent('paste', { source: 'kbd' })
}

Wyświetl plik

@ -56,6 +56,7 @@ function getTypefaces(assetUrls: TLEditorAssetUrls) {
}
}
/** @public */
export function usePreloadAssets(assetUrls: TLEditorAssetUrls) {
const typefaces = useMemo(() => getTypefaces(assetUrls), [assetUrls])

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

@ -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

@ -6,7 +6,7 @@ import { execSync } from 'child_process'
import { appendFileSync, existsSync, readdirSync, writeFileSync } from 'fs'
import path, { join } from 'path'
import { PassThrough } from 'stream'
import tar from 'tar'
import * as tar from 'tar'
import { exec } from './lib/exec'
import { makeEnv } from './lib/makeEnv'
import { nicelog } from './lib/nicelog'
@ -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

@ -32,7 +32,6 @@
"@aws-sdk/lib-storage": "^3.440.0",
"@types/is-ci": "^3.0.0",
"@types/node": "~20.11",
"@types/tar": "^6.1.11",
"@typescript-eslint/utils": "^5.59.0",
"ast-types": "^0.14.2",
"cross-fetch": "^3.1.5",
@ -59,7 +58,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"
}
}

126
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"
@ -7570,7 +7579,6 @@ __metadata:
"@types/is-ci": "npm:^3.0.0"
"@types/minimist": "npm:^1.2.5"
"@types/node": "npm:~20.11"
"@types/tar": "npm:^6.1.11"
"@types/tmp": "npm:^0.2.6"
"@typescript-eslint/utils": "npm:^5.59.0"
ast-types: "npm:^0.14.2"
@ -7589,7 +7597,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
@ -8434,16 +8442,6 @@ __metadata:
languageName: node
linkType: hard
"@types/tar@npm:^6.1.11":
version: 6.1.11
resolution: "@types/tar@npm:6.1.11"
dependencies:
"@types/node": "npm:*"
minipass: "npm:^4.0.0"
checksum: 0d54b8acbd7d2fc43bd1097eef5058604a6b0e3a394cf485038303ca3ef39ecb42451c7dc5a2b9b18420e137ef5b2c76ec504e94c2f45010b2c8e8c3a49d9de7
languageName: node
linkType: hard
"@types/testing-library__jest-dom@npm:^5.9.1":
version: 5.14.9
resolution: "@types/testing-library__jest-dom@npm:5.14.9"
@ -10700,6 +10698,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 +14650,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 +16280,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 +17726,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
@ -19117,7 +19122,7 @@ __metadata:
languageName: node
linkType: hard
"minipass@npm:^4.0.0, minipass@npm:^4.2.4":
"minipass@npm:^4.2.4":
version: 4.2.8
resolution: "minipass@npm:4.2.8"
checksum: e148eb6dcb85c980234cad889139ef8ddf9d5bdac534f4f0268446c8792dd4c74f4502479be48de3c1cce2f6450f6da4d0d4a86405a8a12be04c1c36b339569a
@ -19131,7 +19136,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 +19153,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 +19179,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 +20351,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 +22069,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 +23413,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 +23427,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 +25013,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 +25048,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: caa40343c2c4e6d8e257fccb4c3029f62909c319a86063ce727ed550925c0a834460b0d1ca20c4d6c915f35302aa1052f6ec5193099a47ce21d74b9b817e69e1
checksum: 26342c8dde540e4161fdad2c9c8f2f0e23567f051c7a40abb8e4796d6c4292fbd118ab7a4ac252515e78c4f99525b557731e6117287b2bccde0ea61d73bcff27
languageName: node
linkType: hard
@ -25666,6 +25715,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"