more renaming, docs

pull/3282/head
Steve Ruiz 2024-04-22 15:49:32 +01:00
rodzic 7230a90e59
commit 10f94eafe9
11 zmienionych plików z 578 dodań i 217 usunięć

Wyświetl plik

@ -49,16 +49,76 @@ const {
You can use the editor's camera options to configure the behavior of the editor's camera. There are many options available.
You can set the camera options using the [Editor#setCameraOptions](?) method. You can get the current camera options using the [Editor#cameraOptions](?) property.
### `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 `'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.
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.
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. |

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

@ -18,8 +18,8 @@ const CAMERA_OPTIONS: TLCameraOptions = {
zoomSpeed: 1,
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
constraints: {
defaultZoom: 'fit-max',
zoomBehavior: 'fit',
initialZoom: 'fit-max',
baseZoom: 'fit-max',
bounds: {
x: 0,
y: 0,
@ -283,15 +283,15 @@ const CameraOptionsControlPanel = track(() => {
</select>
{constraints ? (
<>
<label htmlFor="defaultzoom">Default Zoom</label>
<label htmlFor="initialZoom">Initial Zoom</label>
<select
name="defaultzoom"
value={constraints.defaultZoom}
name="initialZoom"
value={constraints.initialZoom}
onChange={(e) => {
updateOptions({
constraints: {
...constraints,
defaultZoom: e.target.value as any,
initialZoom: e.target.value as any,
},
})
}}
@ -302,20 +302,23 @@ const CameraOptionsControlPanel = track(() => {
<option>fit-y</option>
<option>default</option>
</select>
<label htmlFor="fit">Zoom Behavior</label>
<label htmlFor="zoomBehavior">Base Zoom</label>
<select
name="zoomBehavior"
value={constraints.zoomBehavior}
value={constraints.baseZoom}
onChange={(e) => {
updateOptions({
constraints: {
...constraints,
zoomBehavior: e.target.value as any,
baseZoom: e.target.value as any,
},
})
}}
>
<option>fit</option>
<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>
@ -392,9 +395,9 @@ const CameraOptionsControlPanel = track(() => {
})
}}
/>
<label htmlFor="fitx">Fit X</label>
<label htmlFor="behaviorX">Behavior X</label>
<select
name="fitx"
name="behaviorX"
value={(constraints.behavior as { x: any; y: any }).x}
onChange={(e) => {
setCameraOptions({
@ -414,9 +417,9 @@ 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"
name="behaviorY"
value={(constraints.behavior as { x: any; y: any }).y}
onChange={(e) => {
setCameraOptions({

Wyświetl plik

@ -133,8 +133,8 @@ export function ImageAnnotationEditor({
editor.setCameraOptions(
{
constraints: {
defaultZoom: 'fit-max',
zoomBehavior: 'default',
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 },

Wyświetl plik

@ -692,10 +692,8 @@ 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(opts?: {
reset: boolean;
}): number;
getCameraOptions(): TLCameraOptions;
getCameraState(): "idle" | "moving";
getCanRedo(): boolean;
@ -734,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;
@ -2011,15 +2010,15 @@ export type TLCameraMoveOptions = Partial<{
export type TLCameraOptions = {
wheelBehavior: 'none' | 'pan' | 'zoom';
constraints?: {
behavior: 'contain' | 'fixed' | 'inside' | 'outside' | {
x: 'contain' | 'fixed' | 'inside' | 'outside';
y: 'contain' | 'fixed' | 'inside' | 'outside';
behavior: 'contain' | 'fixed' | 'free' | 'inside' | 'outside' | {
x: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
y: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
};
zoomBehavior: 'default' | 'fit';
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;
defaultZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';
};
panSpeed: number;
zoomSpeed: number;

Wyświetl plik

@ -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,54 +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(opts?: "
},
{
"kind": "Content",
"text": "{\n reset: boolean;\n }"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "number"
},
{
"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": "getCameraFitZoom"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)",
@ -11266,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)",
@ -37064,7 +37078,7 @@
},
{
"kind": "Content",
"text": "{\n wheelBehavior: 'none' | 'pan' | 'zoom';\n constraints?: {\n behavior: 'contain' | 'fixed' | 'inside' | 'outside' | {\n x: 'contain' | 'fixed' | 'inside' | 'outside';\n y: 'contain' | 'fixed' | 'inside' | 'outside';\n };\n zoomBehavior: 'default' | 'fit';\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",
@ -37073,7 +37087,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",
@ -37091,7 +37105,7 @@
},
{
"kind": "Content",
"text": ";\n defaultZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-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

@ -2064,36 +2064,25 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Get the zoom level that would fit the camera to the current constraints.
* Get the camera's initial or reset zoom level.
*
* @example
* ```ts
* editor.getCameraFitZoom()
* editor.getInitialZoom()
* ```
*
* @public */
getCameraFitZoom(opts = {} as { reset: boolean }) {
getInitialZoom() {
const cameraOptions = this.getCameraOptions()
if (
// If no camera constraints are provided, the default zoom is 100%
!cameraOptions.constraints ||
// When defaultZoom is default, the default zoom is 100%
cameraOptions.constraints.defaultZoom === 'default' ||
// When zoomBehavior is default, we ignore the default zoom and use 100% as the fit zoom
(!opts.reset && cameraOptions.constraints.zoomBehavior === 'default')
) {
return 1
}
// If no camera constraints are provided, the default zoom is 100%
if (!cameraOptions.constraints) return 1
const { padding } = cameraOptions.constraints
const vsb = this.getViewportScreenBounds()
const py = Math.min(padding.y, vsb.w / 2)
const px = Math.min(padding.x, vsb.h / 2)
const bounds = Box.From(cameraOptions.constraints.bounds)
const zx = (vsb.w - px * 2) / bounds.w
const zy = (vsb.h - py * 2) / bounds.h
// When defaultZoom is default, the default zoom is 100%
if (cameraOptions.constraints.initialZoom === 'default') return 1
switch (cameraOptions.constraints.defaultZoom) {
const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
switch (cameraOptions.constraints.initialZoom) {
case 'fit-min': {
return Math.max(zx, zy)
}
@ -2106,9 +2095,46 @@ export class Editor extends EventEmitter<TLEventMap> {
case 'fit-y': {
return zy
}
// none is accounted-for above
default: {
throw exhaustiveSwitchError(cameraOptions.constraints.defaultZoom)
throw exhaustiveSwitchError(cameraOptions.constraints.initialZoom)
}
}
}
/**
* Get the camera's base level for calculating actual zoom levels based on the zoom steps.
*
* @example
* ```ts
* editor.getBaseZoom()
* ```
*
* @public */
getBaseZoom() {
const cameraOptions = this.getCameraOptions()
// If no camera constraints are provided, the default zoom is 100%
if (!cameraOptions.constraints) return 1
// When defaultZoom is default, the default zoom is 100%
if (cameraOptions.constraints.baseZoom === 'default') return 1
const { zx, zy } = getCameraFitXFitY(this, cameraOptions)
switch (cameraOptions.constraints.baseZoom) {
case 'fit-min': {
return Math.max(zx, zy)
}
case 'fit-max': {
return Math.min(zx, zy)
}
case 'fit-x': {
return zx
}
case 'fit-y': {
return zy
}
default: {
throw exhaustiveSwitchError(cameraOptions.constraints.baseZoom)
}
}
}
@ -2189,32 +2215,12 @@ export class Editor extends EventEmitter<TLEventMap> {
const zx = (vsb.w - px * 2) / bounds.w
const zy = (vsb.h - py * 2) / bounds.h
let fitZoom = 1
switch (cameraOptions.constraints.defaultZoom) {
case 'fit-min': {
fitZoom = Math.max(zx, zy)
break
}
case 'fit-max': {
fitZoom = Math.min(zx, zy)
break
}
case 'fit-x': {
fitZoom = zx
break
}
case 'fit-y': {
fitZoom = zy
break
}
}
const maxZ = zoomMax * fitZoom
const minZ = zoomMin * fitZoom
const baseZoom = this.getBaseZoom()
const maxZ = zoomMax * baseZoom
const minZ = zoomMin * baseZoom
if (opts?.reset) {
z = fitZoom
z = this.getInitialZoom()
}
if (z < minZ || z > maxZ) {
@ -2277,6 +2283,13 @@ export class Editor extends EventEmitter<TLEventMap> {
x = clamp(x, px / z - bounds.w, (vsb.w - px) / z)
break
}
case 'free': {
// noop, use whatever x is provided
break
}
default: {
throw exhaustiveSwitchError(behaviorX)
}
}
// y axis
@ -2300,19 +2313,22 @@ export class Editor extends EventEmitter<TLEventMap> {
y = clamp(y, py / z - bounds.h, (vsb.h - py) / z)
break
}
case 'free': {
// noop, use whatever x is provided
break
}
default: {
throw exhaustiveSwitchError(behaviorY)
}
}
}
} else {
// constrain the zoom, preserving the center
if (z > zoomMax || z < zoomMin) {
const { x: cx, y: cy, z: cz } = currentCamera
const cxA = -cx + vsb.w / cz / 2
const cyA = -cy + vsb.h / cz / 2
z = clamp(z, zoomMin, zoomMax)
const cxB = -cx + vsb.w / z / 2
const cyB = -cy + vsb.h / z / 2
x = cx + cxB - cxA
y = cy + cyB - cyA
x = cx + (-cx + vsb.w / z / 2) - (-cx + vsb.w / cz / 2)
y = cy + (-cy + vsb.h / z / 2) - (-cy + vsb.h / cz / 2)
}
}
}
@ -2499,9 +2515,9 @@ export class Editor extends EventEmitter<TLEventMap> {
if (constraints) {
// For non-infinite fit, we'll set the camera to the natural zoom level...
// unless it's already there, in which case we'll set zoom to 100%
const fitZoom = this.getCameraFitZoom({ reset: true })
if (cz !== fitZoom) {
z = fitZoom
const initialZoom = this.getInitialZoom()
if (cz !== initialZoom) {
z = initialZoom
}
}
@ -2534,11 +2550,11 @@ export class Editor extends EventEmitter<TLEventMap> {
const { zoomSteps } = this.getCameraOptions()
if (zoomSteps !== null && zoomSteps.length > 1) {
const fitZoom = this.getCameraFitZoom()
let zoom = last(zoomSteps)! * fitZoom
const baseZoom = this.getBaseZoom()
let zoom = last(zoomSteps)! * baseZoom
for (let i = 1; i < zoomSteps.length; i++) {
const z1 = zoomSteps[i - 1] * fitZoom
const z2 = zoomSteps[i] * fitZoom
const z1 = zoomSteps[i - 1] * baseZoom
const z2 = zoomSteps[i] * baseZoom
if (z2 - cz <= (z2 - z1) / 2) continue
zoom = z2
break
@ -2576,22 +2592,22 @@ export class Editor extends EventEmitter<TLEventMap> {
const { zoomSteps } = this.getCameraOptions()
if (zoomSteps !== null && zoomSteps.length > 1) {
const fitZoom = this.getCameraFitZoom()
const baseZoom = this.getBaseZoom()
const { x: cx, y: cy, z: cz } = this.getCamera()
let zoom = zoomSteps[0] * fitZoom
let zoom = zoomSteps[0] * baseZoom
for (let i = zoomSteps.length - 2; i > 0; i--) {
const z1 = zoomSteps[i - 1] * fitZoom
const z2 = zoomSteps[i] * fitZoom
const z1 = zoomSteps[i - 1] * baseZoom
const z2 = zoomSteps[i] * baseZoom
if (z2 - cz >= (z2 - z1) / 2) continue
zoom = z1
break
}
this.setCamera(
{
x: cx + (point.x / zoom - point.x) - (point.x / cz - point.x),
y: cy + (point.y / zoom - point.y) - (point.y / cz - point.y),
z: zoom,
},
new Vec(
cx + (point.x / zoom - point.x) - (point.x / cz - point.x),
cy + (point.y / zoom - point.y) - (point.y / cz - point.y),
zoom
),
opts
)
}
@ -2709,7 +2725,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28)
const fitZoom = this.getCameraFitZoom()
const baseZoom = this.getBaseZoom()
const { zoomSteps } = this.getCameraOptions()
const zoomMin = zoomSteps[0]
const zoomMax = last(zoomSteps)!
@ -2719,8 +2735,8 @@ export class Editor extends EventEmitter<TLEventMap> {
(viewportScreenBounds.width - inset) / bounds.w,
(viewportScreenBounds.height - inset) / bounds.h
),
zoomMin * fitZoom,
zoomMax * fitZoom
zoomMin * baseZoom,
zoomMax * baseZoom
)
if (opts?.targetZoom !== undefined) {
@ -3012,6 +3028,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
this._animateToViewport(targetViewportPage, opts)
return this
}
@ -3317,11 +3334,11 @@ export class Editor extends EventEmitter<TLEventMap> {
? Math.min(width / desiredWidth, height / desiredHeight)
: height / desiredHeight
const fitZoom = this.getCameraFitZoom()
const baseZoom = this.getBaseZoom()
const { zoomSteps } = this.getCameraOptions()
const zoomMin = zoomSteps[0]
const zoomMax = last(zoomSteps)!
const targetZoom = clamp(this.getCamera().z * ratio, zoomMin * fitZoom, zoomMax * fitZoom)
const targetZoom = clamp(this.getCamera().z * ratio, zoomMin * baseZoom, zoomMax * baseZoom)
const targetWidth = this.getViewportScreenBounds().w / targetZoom
const targetHeight = this.getViewportScreenBounds().h / targetZoom
@ -8940,44 +8957,44 @@ export class Editor extends EventEmitter<TLEventMap> {
} else {
const { panSpeed, zoomSpeed, wheelBehavior } = cameraOptions
if (wheelBehavior === 'none') return
// Stop any camera animation
this.stopCameraAnimation()
// Stop following any following user
if (instanceState.followingUserId) {
this.stopFollowingUser()
}
const { x: cx, y: cy, z: cz } = camera
const { x: dx, y: dy, z: dz = 0 } = info.delta
// If the camera behavior is "zoom" and the ctrl key is presssed, then pan;
// If the camera behavior is "pan" and the ctrl key is not pressed, then zoom
const behavior =
wheelBehavior === 'pan' ? (inputs.ctrlKey ? 'zoom' : 'pan') : wheelBehavior
switch (behavior) {
case 'zoom': {
// Zoom in on current screen point using the wheel delta
const { x, y } = this.inputs.currentScreenPoint
const zoom = cz + (dz ?? 0) * zoomSpeed * cz
this._setCamera(
new Vec(
cx + (x / zoom - x) - (x / cz - x),
cy + (y / zoom - y) - (y / cz - y),
zoom
),
{ immediate: true }
)
return
if (wheelBehavior !== 'none') {
// Stop any camera animation
this.stopCameraAnimation()
// Stop following any following user
if (instanceState.followingUserId) {
this.stopFollowingUser()
}
case 'pan': {
// Pan the camera based on the wheel delta
this._setCamera(new Vec(cx + (dx * panSpeed) / cz, cy + (dy * panSpeed) / cz, cz), {
immediate: true,
})
return
const { x: cx, y: cy, z: cz } = camera
const { x: dx, y: dy, z: dz = 0 } = info.delta
// If the camera behavior is "zoom" and the ctrl key is presssed, then pan;
// If the camera behavior is "pan" and the ctrl key is not pressed, then zoom
const behavior =
wheelBehavior === 'pan' ? (inputs.ctrlKey ? 'zoom' : 'pan') : wheelBehavior
switch (behavior) {
case 'zoom': {
// Zoom in on current screen point using the wheel delta
const { x, y } = this.inputs.currentScreenPoint
const zoom = cz + (dz ?? 0) * zoomSpeed * cz
this._setCamera(
new Vec(
cx + (x / zoom - x) - (x / cz - x),
cy + (y / zoom - y) - (y / cz - y),
zoom
),
{ immediate: true }
)
return
}
case 'pan': {
// Pan the camera based on the wheel delta
this._setCamera(new Vec(cx + (dx * panSpeed) / cz, cy + (dy * panSpeed) / cz, cz), {
immediate: true,
})
return
}
}
}
}
@ -9267,3 +9284,15 @@ function pushShapeWithDescendants(editor: Editor, id: TLShapeId, result: TLShape
pushShapeWithDescendants(editor, childIds[i], result)
}
}
function getCameraFitXFitY(editor: Editor, cameraOptions: TLCameraOptions) {
if (!cameraOptions.constraints) throw Error('Should have constraints here')
const {
padding: { x: px, y: py },
} = cameraOptions.constraints
const vsb = editor.getViewportScreenBounds()
const bounds = Box.From(cameraOptions.constraints.bounds)
const zx = (vsb.w - px * 2) / bounds.w
const zy = (vsb.h - py * 2) / bounds.h
return { zx, zy }
}

Wyświetl plik

@ -48,25 +48,26 @@ export type TLCameraOptions = {
isLocked: boolean
/** The camera constraints */
constraints?: {
/** Which dimension to fit when the camera is reset. */
defaultZoom: 'fit-min' | 'fit-max' | 'fit-x' | 'fit-y' | 'default'
/** The behavior for the zoom. When 'fit', the steps will be a multiplier of the default zoom. */
zoomBehavior: 'fit' | 'default'
/** The behavior for the constraints on the x axis. */
behavior:
| 'contain'
| 'inside'
| 'outside'
| 'fixed'
| {
x: 'contain' | 'inside' | 'outside' | 'fixed'
y: 'contain' | 'inside' | 'outside' | 'fixed'
}
/** The bounds (in page space) of the constrained space */
bounds: BoxModel
/** The padding inside of the viewport (in screen space) */
padding: VecLike
/** 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

@ -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,3 @@
import { DEFAULT_CAMERA_OPTIONS } from '@tldraw/editor'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
@ -8,7 +7,7 @@ beforeEach(() => {
})
it('zooms out and in by increments', () => {
const cameraOptions = DEFAULT_CAMERA_OPTIONS
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)
})