Initial OSS commit
This commit is contained in:
commit
4a7303f40a
391 changed files with 245738 additions and 0 deletions
203
theatre/core/LICENSE
Normal file
203
theatre/core/LICENSE
Normal file
|
@ -0,0 +1,203 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
19
theatre/core/package.json
Normal file
19
theatre/core/package.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@theatre/core",
|
||||
"main": "dist/index.js",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist/index.js",
|
||||
"dist/index.d.ts",
|
||||
"index.js.map"
|
||||
],
|
||||
"scripts": {
|
||||
"prepack": "cd .. && yarn run prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@theatre/dataverse": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.3.2"
|
||||
}
|
||||
}
|
12
theatre/core/src/.eslintrc.js
Normal file
12
theatre/core/src/.eslintrc.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: `ImportDeclaration[importKind!='type'][source.value=/@theatre\\u002Fstudio/]`,
|
||||
message:
|
||||
'@theatre/core may not import @theatre/studio modules except via type imports.',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
31
theatre/core/src/CoreBundle.ts
Normal file
31
theatre/core/src/CoreBundle.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type Studio from '@theatre/studio/Studio'
|
||||
import projectsSingleton from './projects/projectsSingleton'
|
||||
|
||||
export type CoreBits = {
|
||||
projectsP: typeof projectsSingleton.atom.pointer.projects
|
||||
}
|
||||
|
||||
export default class CoreBundle {
|
||||
private _studio: Studio | undefined = undefined
|
||||
constructor() {}
|
||||
|
||||
get type(): 'Theatre_CoreBundle' {
|
||||
return 'Theatre_CoreBundle'
|
||||
}
|
||||
|
||||
get version() {
|
||||
return $env.version
|
||||
}
|
||||
|
||||
getBitsForStudio(studio: Studio, callback: (bits: CoreBits) => void) {
|
||||
if (this._studio) {
|
||||
throw new Error(`@theatre/core is already attached to @theatre/studio`)
|
||||
}
|
||||
this._studio = studio
|
||||
const bits: CoreBits = {
|
||||
projectsP: projectsSingleton.atom.pointer.projects,
|
||||
}
|
||||
|
||||
callback(bits)
|
||||
}
|
||||
}
|
80
theatre/core/src/coreExports.ts
Normal file
80
theatre/core/src/coreExports.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import projectsSingleton from '@theatre/core/projects/projectsSingleton'
|
||||
import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
|
||||
import type {
|
||||
IProject,
|
||||
IProjectConfig,
|
||||
} from '@theatre/core/projects/TheatreProject'
|
||||
import TheatreProject from '@theatre/core/projects/TheatreProject'
|
||||
import * as types from '@theatre/shared/src/propTypes'
|
||||
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
|
||||
import {validateName} from '@theatre/shared/utils/sanitizers'
|
||||
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
|
||||
export {types}
|
||||
|
||||
export function getProject(id: string, config: IProjectConfig = {}): IProject {
|
||||
const {...restOfConfig} = config
|
||||
if (projectsSingleton.has(id)) {
|
||||
return projectsSingleton.get(id)!.publicApi
|
||||
}
|
||||
|
||||
if ($env.NODE_ENV === 'development') {
|
||||
validateName(id, 'projectName in Theatre.getProject(projectName)', true)
|
||||
validateProjectIdOrThrow(id)
|
||||
}
|
||||
|
||||
if (config.state) {
|
||||
if ($env.NODE_ENV === 'development') {
|
||||
shallowValidateOnDiskState(id, config.state)
|
||||
} else {
|
||||
deepValidateOnDiskState(id, config.state)
|
||||
}
|
||||
}
|
||||
|
||||
return new TheatreProject(id, restOfConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight validator that only makes sure the state's definitionVersion is correct.
|
||||
* Does not do a thorough validation of the state.
|
||||
*/
|
||||
const shallowValidateOnDiskState = (projectId: string, s: OnDiskState) => {
|
||||
if (
|
||||
Array.isArray(s) ||
|
||||
s == null ||
|
||||
s.definitionVersion !== $env.currentProjectStateDefinitionVersion
|
||||
) {
|
||||
throw new InvalidArgumentError(
|
||||
`Error validating conf.state in Theatre.getProject(${JSON.stringify(
|
||||
projectId,
|
||||
)}, conf). The state seems to be formatted in a way that is unreadable to Theatre.js. Read more at https://docs.theatrejs.com`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const deepValidateOnDiskState = (projectId: string, s: OnDiskState) => {
|
||||
shallowValidateOnDiskState(projectId, s)
|
||||
// @TODO do a deep validation here
|
||||
}
|
||||
|
||||
const validateProjectIdOrThrow = (value: string) => {
|
||||
if (typeof value !== 'string') {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument 'name' in \`Theatre.getProject(name, ...)\` must be a string. Instead, it was ${userReadableTypeOfValue(
|
||||
value,
|
||||
)}.`,
|
||||
)
|
||||
}
|
||||
|
||||
const idTrimmed = value.trim()
|
||||
if (idTrimmed.length !== value.length) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument 'name' in \`Theatre.getProject("${value}", ...)\` should not have surrounding whitespace.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (idTrimmed.length < 3) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument 'name' in \`Theatre.getProject("${value}", ...)\` should be at least 3 characters long.`,
|
||||
)
|
||||
}
|
||||
}
|
14
theatre/core/src/coreTicker.ts
Normal file
14
theatre/core/src/coreTicker.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Ticker} from '@theatre/dataverse'
|
||||
|
||||
const coreTicker = new Ticker()
|
||||
|
||||
export default coreTicker
|
||||
|
||||
/**
|
||||
* @todo users should also be able to define their own ticker.
|
||||
*/
|
||||
const onAnimationFrame = (t: number) => {
|
||||
coreTicker.tick(t)
|
||||
window.requestAnimationFrame(onAnimationFrame)
|
||||
}
|
||||
window.requestAnimationFrame(onAnimationFrame)
|
56
theatre/core/src/index.ts
Normal file
56
theatre/core/src/index.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
export * from './coreExports'
|
||||
export type {
|
||||
IProject,
|
||||
IProjectConfig,
|
||||
} from '@theatre/core/projects/TheatreProject'
|
||||
export type {ISequence} from '@theatre/core/sequences/TheatreSequence'
|
||||
export type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject'
|
||||
export type {ISheet} from '@theatre/core/sheets/TheatreSheet'
|
||||
import * as globalVariableNames from '@theatre/shared/globalVariableNames'
|
||||
import type StudioBundle from '@theatre/studio/StudioBundle'
|
||||
import CoreBundle from './CoreBundle'
|
||||
|
||||
registerCoreBundle()
|
||||
|
||||
function registerCoreBundle() {
|
||||
if (typeof window == 'undefined') return
|
||||
|
||||
// @ts-ignore ignore
|
||||
const existingBundle = window[globalVariableNames.coreBundle]
|
||||
|
||||
if (typeof existingBundle !== 'undefined') {
|
||||
if (
|
||||
typeof existingBundle === 'object' &&
|
||||
existingBundle &&
|
||||
typeof existingBundle.version === 'string'
|
||||
) {
|
||||
throw new Error(
|
||||
`It seems that the module '@theatre/core' is loaded more than once. This could have two possible causes:\n` +
|
||||
`1. You might have two separate versions of theatre in node_modules.\n` +
|
||||
`2. Or this might be a bundling misconfiguration, in case you're using a bundler like Webpack/ESBuild/Rollup.\n\n` +
|
||||
`Note that it **is okay** to import '@theatre/core' multiple times. But those imports should point to the same module.`,
|
||||
)
|
||||
} else {
|
||||
throw new Error(
|
||||
`The variable window.${globalVariableNames.coreBundle} seems to be already set by a module other than @theatre/core.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const coreBundle = new CoreBundle()
|
||||
|
||||
// @ts-ignore ignore
|
||||
window[globalVariableNames.coreBundle] = coreBundle
|
||||
|
||||
const possibleExistingStudioBundle: undefined | StudioBundle =
|
||||
// @ts-ignore ignore
|
||||
window[globalVariableNames.studioBundle]
|
||||
|
||||
if (
|
||||
possibleExistingStudioBundle &&
|
||||
possibleExistingStudioBundle !== null &&
|
||||
possibleExistingStudioBundle.type === 'Theatre_StudioBundle'
|
||||
) {
|
||||
possibleExistingStudioBundle.registerCoreBundle(coreBundle)
|
||||
}
|
||||
}
|
161
theatre/core/src/projects/Project.ts
Normal file
161
theatre/core/src/projects/Project.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import type {OnDiskState} from '@theatre/core/projects/store/storeTypes'
|
||||
import type TheatreProject from '@theatre/core/projects/TheatreProject'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import SheetTemplate from '@theatre/core/sheets/SheetTemplate'
|
||||
import type Studio from '@theatre/studio/Studio'
|
||||
import type {ProjectAddress} from '@theatre/shared/utils/addresses'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
import {PointerProxy} from '@theatre/dataverse'
|
||||
import {Atom} from '@theatre/dataverse'
|
||||
import initialiseProjectState from './initialiseProjectState'
|
||||
import projectsSingleton from './projectsSingleton'
|
||||
import type {ProjectState} from './store/storeTypes'
|
||||
import type {Deferred} from '@theatre/shared/utils/defer'
|
||||
import {defer} from '@theatre/shared/utils/defer'
|
||||
|
||||
export type Conf = Partial<{
|
||||
state: OnDiskState
|
||||
}>
|
||||
|
||||
export default class Project {
|
||||
readonly pointers: {
|
||||
historic: Pointer<ProjectState['historic']>
|
||||
ahistoric: Pointer<ProjectState['ahistoric']>
|
||||
ephemeral: Pointer<ProjectState['ephemeral']>
|
||||
}
|
||||
|
||||
private readonly _pointerProxies: {
|
||||
historic: PointerProxy<ProjectState['historic']>
|
||||
ahistoric: PointerProxy<ProjectState['ahistoric']>
|
||||
ephemeral: PointerProxy<ProjectState['ephemeral']>
|
||||
}
|
||||
|
||||
readonly address: ProjectAddress
|
||||
|
||||
private readonly _readyDeferred: Deferred<undefined>
|
||||
|
||||
private _sheetTemplates = new Atom<{
|
||||
[sheetId: string]: SheetTemplate | undefined
|
||||
}>({})
|
||||
sheetTemplatesP = this._sheetTemplates.pointer
|
||||
private _studio: Studio | undefined
|
||||
|
||||
type: 'Theatre_Project' = 'Theatre_Project'
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
readonly config: Conf = {},
|
||||
readonly publicApi: TheatreProject,
|
||||
) {
|
||||
this.address = {projectId: id}
|
||||
|
||||
const onDiskStateAtom = new Atom<ProjectState>({
|
||||
ahistoric: {
|
||||
ahistoricStuff: '',
|
||||
},
|
||||
historic: config.state ?? {
|
||||
sheetsById: {},
|
||||
definitionVersion: $env.currentProjectStateDefinitionVersion,
|
||||
},
|
||||
ephemeral: {
|
||||
loadingState: {
|
||||
type: 'loaded',
|
||||
},
|
||||
lastExportedObject: null,
|
||||
},
|
||||
})
|
||||
|
||||
this._pointerProxies = {
|
||||
historic: new PointerProxy(onDiskStateAtom.pointer.historic),
|
||||
ahistoric: new PointerProxy(onDiskStateAtom.pointer.ahistoric),
|
||||
ephemeral: new PointerProxy(onDiskStateAtom.pointer.ephemeral),
|
||||
}
|
||||
|
||||
this.pointers = {
|
||||
historic: this._pointerProxies.historic.pointer,
|
||||
ahistoric: this._pointerProxies.ahistoric.pointer,
|
||||
ephemeral: this._pointerProxies.ephemeral.pointer,
|
||||
}
|
||||
|
||||
projectsSingleton.add(id, this)
|
||||
|
||||
this._readyDeferred = defer()
|
||||
|
||||
if (config.state) {
|
||||
setTimeout(() => {
|
||||
// The user has provided config.state but in case @theatre/studio is loaded,
|
||||
// let's give it one tick to attach itself
|
||||
if (!this._studio) {
|
||||
this._readyDeferred.resolve(undefined)
|
||||
}
|
||||
}, 0)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (!this._studio) {
|
||||
throw new Error(
|
||||
`Argument config.state in Theatre.getProject("${id}", config) is empty. This is fine ` +
|
||||
`while you are using @theatre/core along with @theatre/sutdio. But since @theatre/studio ` +
|
||||
`is not loaded, the state of project "${id}" will be empty.\n\n` +
|
||||
`To fix this, you need to add @theatre/studio into the bundle and export ` +
|
||||
`the projet's state. Learn how to do that at https://docs.theatrejs.com/export.html`,
|
||||
)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
attachToStudio(studio: Studio) {
|
||||
if (this._studio) {
|
||||
if (this._studio !== studio) {
|
||||
throw new Error(
|
||||
`Project ${this.address.projectId} is already attached to studio ${this._studio.address.studioId}`,
|
||||
)
|
||||
} else {
|
||||
console.warn(
|
||||
`Project ${this.address.projectId} is already attached to studio ${this._studio.address.studioId}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
this._studio = studio
|
||||
|
||||
studio.initialized.then(async () => {
|
||||
await initialiseProjectState(studio, this, this.config.state)
|
||||
|
||||
this._pointerProxies.historic.setPointer(
|
||||
studio.atomP.historic.coreByProject[this.address.projectId],
|
||||
)
|
||||
this._pointerProxies.ahistoric.setPointer(
|
||||
studio.atomP.ahistoric.coreByProject[this.address.projectId],
|
||||
)
|
||||
this._pointerProxies.ephemeral.setPointer(
|
||||
studio.atomP.ephemeral.coreByProject[this.address.projectId],
|
||||
)
|
||||
|
||||
this._readyDeferred.resolve(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
get isAttachedToStudio() {
|
||||
return !!this._studio
|
||||
}
|
||||
|
||||
get ready() {
|
||||
return this._readyDeferred.promise
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this._readyDeferred.status === 'resolved'
|
||||
}
|
||||
|
||||
getOrCreateSheet(sheetId: string, instanceId: string = 'default'): Sheet {
|
||||
let template = this._sheetTemplates.getState()[sheetId]
|
||||
|
||||
if (!template) {
|
||||
template = new SheetTemplate(this, sheetId)
|
||||
this._sheetTemplates.setIn([sheetId], template)
|
||||
}
|
||||
|
||||
return template.getInstance(instanceId)
|
||||
}
|
||||
}
|
66
theatre/core/src/projects/TheatreProject.ts
Normal file
66
theatre/core/src/projects/TheatreProject.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import {privateAPI, setPrivateAPI} from '@theatre/shared/privateAPIs'
|
||||
import Project from '@theatre/core/projects/Project'
|
||||
import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
|
||||
import type {ProjectAddress} from '@theatre/shared/utils/addresses'
|
||||
import {validateName} from '@theatre/shared/utils/sanitizers'
|
||||
import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
|
||||
export type IProjectConfig = Partial<{
|
||||
state: $IntentionalAny
|
||||
}>
|
||||
|
||||
export interface IProject {
|
||||
readonly type: 'Theatre_Project_PublicAPI'
|
||||
readonly ready: Promise<void>
|
||||
/**
|
||||
* Shows whether the project is ready to be used.
|
||||
* Better to use IProject.ready, which is a promise that will
|
||||
* resolve when the project is ready.
|
||||
*/
|
||||
readonly isReady: boolean
|
||||
readonly address: ProjectAddress
|
||||
sheet(sheetId: string, instanceId?: string): ISheet
|
||||
}
|
||||
|
||||
export default class TheatreProject implements IProject {
|
||||
get type(): 'Theatre_Project_PublicAPI' {
|
||||
return 'Theatre_Project_PublicAPI'
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(id: string, config: IProjectConfig = {}) {
|
||||
setPrivateAPI(this, new Project(id, config, this))
|
||||
}
|
||||
|
||||
get ready(): Promise<void> {
|
||||
return privateAPI(this).ready
|
||||
}
|
||||
|
||||
get isReady(): boolean {
|
||||
return privateAPI(this).isReady()
|
||||
}
|
||||
|
||||
get address(): ProjectAddress {
|
||||
return {...privateAPI(this).address}
|
||||
}
|
||||
|
||||
sheet(sheetId: string, instanceId: string = 'default'): ISheet {
|
||||
const sanitizedPath = validateAndSanitiseSlashedPathOrThrow(
|
||||
sheetId,
|
||||
'project.sheet',
|
||||
)
|
||||
|
||||
if (!$env.isCore) {
|
||||
validateName(
|
||||
instanceId,
|
||||
'instanceId in project.sheet(sheetId, instanceId)',
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
return privateAPI(this).getOrCreateSheet(sanitizedPath, instanceId)
|
||||
.publicApi
|
||||
}
|
||||
}
|
95
theatre/core/src/projects/initialiseProjectState.ts
Normal file
95
theatre/core/src/projects/initialiseProjectState.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import type Studio from '@theatre/studio/Studio'
|
||||
import delay from '@theatre/shared/utils/delay'
|
||||
import {original} from 'immer'
|
||||
import type Project from './Project'
|
||||
import type {OnDiskState} from './store/storeTypes'
|
||||
|
||||
/**
|
||||
* @todo this could be turned into a simple derivation, like:
|
||||
* editor.isReady: IDerivation<{isReady: true} | {isReady: false, readon: 'conflictBetweenDiskStateAndBrowserState'}>
|
||||
*/
|
||||
export default async function initialiseProjectState(
|
||||
studio: Studio,
|
||||
project: Project,
|
||||
onDiskState: OnDiskState | undefined,
|
||||
) {
|
||||
/*
|
||||
* If in the future we move to IndexedDB to store the state, we'll have
|
||||
* to deal with it being async (as opposed to localStorage that is synchronous.)
|
||||
* so here we're artifically delaying the loading of the state to make sure users
|
||||
* don't count on the state always being already loaded synchronously
|
||||
*/
|
||||
await delay(0)
|
||||
|
||||
studio.transaction(({drafts}) => {
|
||||
const projectId = project.address.projectId
|
||||
|
||||
drafts.ephemeral.coreByProject[projectId] = {
|
||||
lastExportedObject: null,
|
||||
loadingState: {type: 'loading'},
|
||||
}
|
||||
|
||||
drafts.ahistoric.coreByProject[projectId] = {
|
||||
ahistoricStuff: '',
|
||||
}
|
||||
|
||||
function useInitialState() {
|
||||
drafts.ephemeral.coreByProject[projectId].loadingState = {
|
||||
type: 'loaded',
|
||||
}
|
||||
|
||||
drafts.historic.coreByProject[projectId] = {
|
||||
sheetsById: {},
|
||||
definitionVersion: $env.currentProjectStateDefinitionVersion,
|
||||
}
|
||||
}
|
||||
|
||||
function useOnDiskState(state: OnDiskState) {
|
||||
drafts.ephemeral.coreByProject[projectId].loadingState = {
|
||||
type: 'loaded',
|
||||
}
|
||||
|
||||
drafts.historic.coreByProject[projectId] = state
|
||||
}
|
||||
|
||||
function useBrowserState() {
|
||||
drafts.ephemeral.coreByProject[projectId].loadingState = {
|
||||
type: 'loaded',
|
||||
}
|
||||
}
|
||||
|
||||
function browserStateIsNotBasedOnDiskState(onDiskState: OnDiskState) {
|
||||
drafts.ephemeral.coreByProject[projectId].loadingState = {
|
||||
type: 'browserStateIsNotBasedOnDiskState',
|
||||
onDiskState,
|
||||
}
|
||||
}
|
||||
|
||||
const browserState = original(drafts.historic)?.coreByProject[
|
||||
project.address.projectId
|
||||
]
|
||||
|
||||
if (!browserState) {
|
||||
if (!onDiskState) {
|
||||
useInitialState()
|
||||
} else {
|
||||
useOnDiskState(onDiskState)
|
||||
}
|
||||
} else {
|
||||
if (!onDiskState) {
|
||||
useBrowserState()
|
||||
} else {
|
||||
if (
|
||||
!browserState.exportBookkeeping ||
|
||||
browserState.exportBookkeeping.basedOnRevisions.indexOf(
|
||||
onDiskState.exportBookkeeping.revision,
|
||||
) == -1
|
||||
) {
|
||||
browserStateIsNotBasedOnDiskState(onDiskState)
|
||||
} else {
|
||||
useBrowserState()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
30
theatre/core/src/projects/projectsSingleton.ts
Normal file
30
theatre/core/src/projects/projectsSingleton.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import {Atom} from '@theatre/dataverse'
|
||||
import type Project from './Project'
|
||||
|
||||
interface State {
|
||||
projects: Record<string, Project>
|
||||
}
|
||||
|
||||
class ProjectsSingleton {
|
||||
readonly atom = new Atom({projects: {}} as State)
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* We're trusting here that each project id is unique
|
||||
*/
|
||||
add(id: string, project: Project) {
|
||||
this.atom.reduceState(['projects', id], () => project)
|
||||
}
|
||||
|
||||
get(id: string): Project | undefined {
|
||||
return this.atom.getState().projects[id]
|
||||
}
|
||||
|
||||
has(id: string) {
|
||||
return !!this.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new ProjectsSingleton()
|
||||
|
||||
export default singleton
|
49
theatre/core/src/projects/store/storeTypes.ts
Normal file
49
theatre/core/src/projects/store/storeTypes.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import type {StrictRecord} from '@theatre/shared/utils/types'
|
||||
import type {SheetState_Historic} from './types/SheetState_Historic'
|
||||
|
||||
export interface ProjectLoadedState {
|
||||
type: 'loaded'
|
||||
}
|
||||
|
||||
type ProjectLoadingState =
|
||||
| {type: 'loading'}
|
||||
| ProjectLoadedState
|
||||
| {
|
||||
type: 'browserStateIsNotBasedOnDiskState'
|
||||
onDiskState: OnDiskState
|
||||
}
|
||||
|
||||
/**
|
||||
* Ahistoric state is persisted, but its changes
|
||||
* are not undoable.
|
||||
*/
|
||||
export interface ProjectAhistoricState {
|
||||
ahistoricStuff: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Ephemeral state is neither persisted nor undoable
|
||||
*/
|
||||
export interface ProjectEphemeralState {
|
||||
loadingState: ProjectLoadingState
|
||||
lastExportedObject: null | OnDiskState
|
||||
}
|
||||
|
||||
/**
|
||||
* Historic state is both persisted and is undoable
|
||||
*/
|
||||
export interface ProjectState_Historic {
|
||||
sheetsById: StrictRecord<string, SheetState_Historic>
|
||||
exportBookkeeping?: {revision: string; basedOnRevisions: string[]}
|
||||
definitionVersion: string
|
||||
}
|
||||
|
||||
export interface ProjectState {
|
||||
historic: ProjectState_Historic
|
||||
ahistoric: ProjectAhistoricState
|
||||
ephemeral: ProjectEphemeralState
|
||||
}
|
||||
|
||||
export interface OnDiskState extends ProjectState_Historic {
|
||||
exportBookkeeping: {revision: string; basedOnRevisions: string[]}
|
||||
}
|
51
theatre/core/src/projects/store/types/SheetState_Historic.ts
Normal file
51
theatre/core/src/projects/store/types/SheetState_Historic.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import type {KeyframeId, SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import type {SerializableMap, StrictRecord} from '@theatre/shared/utils/types'
|
||||
|
||||
export interface SheetState_Historic {
|
||||
/**
|
||||
* @remarks
|
||||
* Notes for when we implement FSMs:
|
||||
*
|
||||
* Each FSM state will have overrides of its own. Since a state could be a descendant
|
||||
* of another state, it will be able to inherit the overrides from ancestor states.
|
||||
*/
|
||||
staticOverrides: {
|
||||
byObject: StrictRecord<string, SerializableMap>
|
||||
}
|
||||
sequence?: Sequence
|
||||
}
|
||||
|
||||
type Sequence = PositionalSequence
|
||||
|
||||
type PositionalSequence = {
|
||||
type: 'PositionalSequence'
|
||||
length: number
|
||||
/**
|
||||
* If set to, say, 30, then the keyframe editor will try to snap all keyframes
|
||||
* to a 30fps grid
|
||||
*/
|
||||
subUnitsPerUnit: number
|
||||
|
||||
tracksByObject: StrictRecord<
|
||||
string,
|
||||
{
|
||||
trackIdByPropPath: StrictRecord<string, SequenceTrackId>
|
||||
trackData: StrictRecord<SequenceTrackId, TrackData>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export type TrackData = BasicKeyframedTrack
|
||||
|
||||
export type Keyframe = {
|
||||
id: KeyframeId
|
||||
value: number
|
||||
position: number
|
||||
handles: [leftX: number, leftY: number, rightX: number, rightY: number]
|
||||
connectedRight: boolean
|
||||
}
|
||||
|
||||
export type BasicKeyframedTrack = {
|
||||
type: 'BasicKeyframedTrack'
|
||||
keyframes: Keyframe[]
|
||||
}
|
389
theatre/core/src/sequences/Sequence.ts
Normal file
389
theatre/core/src/sequences/Sequence.ts
Normal file
|
@ -0,0 +1,389 @@
|
|||
import type Project from '@theatre/core/projects/Project'
|
||||
import coreTicker from '@theatre/core/coreTicker'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import type {SequenceAddress} from '@theatre/shared/utils/addresses'
|
||||
import didYouMean from '@theatre/shared/utils/didYouMean'
|
||||
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
|
||||
import type {IRange} from '@theatre/shared/utils/types'
|
||||
import type {IBox, IDerivation, Pointer} from '@theatre/dataverse'
|
||||
import {Box, prism, val, valueDerivation} from '@theatre/dataverse'
|
||||
import {padStart} from 'lodash-es'
|
||||
import type {
|
||||
IPlaybackController,
|
||||
IPlaybackState,
|
||||
} from './playbackControllers/DefaultPlaybackController'
|
||||
import DefaultPlaybackController from './playbackControllers/DefaultPlaybackController'
|
||||
import TheatreSequence from './TheatreSequence'
|
||||
import logger from '@theatre/shared/logger'
|
||||
|
||||
export type IPlaybackRange = IRange
|
||||
|
||||
export type IPlaybackDirection =
|
||||
| 'normal'
|
||||
| 'reverse'
|
||||
| 'alternate'
|
||||
| 'alternateReverse'
|
||||
|
||||
const possibleDirections = [
|
||||
'normal',
|
||||
'reverse',
|
||||
'alternate',
|
||||
'alternateReverse',
|
||||
]
|
||||
|
||||
export default class Sequence {
|
||||
public readonly address: SequenceAddress
|
||||
publicApi: TheatreSequence
|
||||
|
||||
private _playbackControllerBox: IBox<IPlaybackController>
|
||||
private _statePointerDerivation: IDerivation<Pointer<IPlaybackState>>
|
||||
private _positionD: IDerivation<number>
|
||||
private _positionFormatterD: IDerivation<ISequencePositionFormatter>
|
||||
_playableRangeD: undefined | IDerivation<{start: number; end: number}>
|
||||
|
||||
constructor(
|
||||
readonly _project: Project,
|
||||
readonly _sheet: Sheet,
|
||||
readonly _lengthD: IDerivation<number>,
|
||||
readonly _subUnitsPerUnitD: IDerivation<number>,
|
||||
playbackController?: IPlaybackController,
|
||||
) {
|
||||
this.address = {...this._sheet.address, sequenceName: 'default'}
|
||||
|
||||
this.publicApi = new TheatreSequence(this)
|
||||
|
||||
this._playbackControllerBox = new Box(
|
||||
playbackController ?? new DefaultPlaybackController(coreTicker),
|
||||
)
|
||||
|
||||
this._statePointerDerivation = this._playbackControllerBox.derivation.map(
|
||||
(playbackController) => playbackController.statePointer,
|
||||
)
|
||||
|
||||
this._positionD = this._statePointerDerivation.flatMap((statePointer) =>
|
||||
valueDerivation(statePointer.position),
|
||||
)
|
||||
|
||||
this._positionFormatterD = this._subUnitsPerUnitD.map(
|
||||
(subUnitsPerUnit) => new TimeBasedPositionFormatter(subUnitsPerUnit),
|
||||
)
|
||||
}
|
||||
|
||||
get positionFormatter(): ISequencePositionFormatter {
|
||||
return this._positionFormatterD.getValue()
|
||||
}
|
||||
|
||||
get derivationToStatePointer() {
|
||||
return this._statePointerDerivation
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._lengthD.getValue()
|
||||
}
|
||||
|
||||
get positionDerivation() {
|
||||
return this._positionD
|
||||
}
|
||||
|
||||
get position() {
|
||||
return this._playbackControllerBox.get().getCurrentPosition()
|
||||
}
|
||||
|
||||
get subUnitsPerUnit(): number {
|
||||
return this._subUnitsPerUnitD.getValue()
|
||||
}
|
||||
|
||||
get positionSnappedToGrid(): number {
|
||||
return this.closestGridPosition(this.position)
|
||||
}
|
||||
|
||||
closestGridPosition(posInUnitSpace: number): number {
|
||||
const subUnitsPerUnit = this.subUnitsPerUnit
|
||||
const gridLength = 1 / subUnitsPerUnit
|
||||
|
||||
return Math.round(posInUnitSpace / gridLength) * gridLength
|
||||
}
|
||||
|
||||
set position(requestedPosition: number) {
|
||||
let position = requestedPosition
|
||||
this.pause()
|
||||
if (!$env.isCore) {
|
||||
if (typeof position !== 'number') {
|
||||
logger.error(
|
||||
`value t in sequence.position = t must be a number. ${typeof position} given`,
|
||||
)
|
||||
position = 0
|
||||
}
|
||||
if (position < 0) {
|
||||
logger.error(
|
||||
`sequence.position must be a positive number. ${position} given`,
|
||||
)
|
||||
position = 0
|
||||
}
|
||||
}
|
||||
if (position > this.length) {
|
||||
position = this.length
|
||||
}
|
||||
const dur = this.length
|
||||
this._playbackControllerBox
|
||||
.get()
|
||||
.gotoPosition(position > dur ? dur : position)
|
||||
}
|
||||
|
||||
getDurationCold() {
|
||||
return this._lengthD.getValue()
|
||||
}
|
||||
|
||||
get playing() {
|
||||
return this._playbackControllerBox.get().playing
|
||||
}
|
||||
|
||||
_makeRangeFromSequenceTemplate(): IDerivation<IPlaybackRange> {
|
||||
return prism(() => {
|
||||
return {
|
||||
start: 0,
|
||||
end: val(this._lengthD),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async play(
|
||||
conf?: Partial<{
|
||||
iterationCount: number
|
||||
range: IPlaybackRange
|
||||
rate: number
|
||||
direction: IPlaybackDirection
|
||||
}>,
|
||||
): Promise<boolean> {
|
||||
const sequenceDuration = this.length
|
||||
const range =
|
||||
conf && conf.range
|
||||
? conf.range
|
||||
: {
|
||||
start: 0,
|
||||
end: sequenceDuration,
|
||||
}
|
||||
|
||||
if (!$env.isCore) {
|
||||
if (typeof range.start !== 'number' || range.start < 0) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.range.start in sequence.play(conf) must be a positive number. ${JSON.stringify(
|
||||
range.start,
|
||||
)} given.`,
|
||||
)
|
||||
}
|
||||
if (range.start >= sequenceDuration) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.range.start in sequence.play(conf) cannot be longer than the duration of the sequence, which is ${sequenceDuration}ms. ${JSON.stringify(
|
||||
range.start,
|
||||
)} given.`,
|
||||
)
|
||||
}
|
||||
if (typeof range.end !== 'number' || range.end <= 0) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.range.end in sequence.play(conf) must be a number larger than zero. ${JSON.stringify(
|
||||
range.end,
|
||||
)} given.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (range.end > sequenceDuration) {
|
||||
logger.warn(
|
||||
`Argument conf.range.end in sequence.play(conf) cannot be longer than the duration of the sequence, which is ${sequenceDuration}ms. ${JSON.stringify(
|
||||
range.end,
|
||||
)} given.`,
|
||||
)
|
||||
range.end = sequenceDuration
|
||||
}
|
||||
|
||||
if (range.end <= range.start) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.range.end in sequence.play(conf) must be larger than conf.range.start. ${JSON.stringify(
|
||||
range,
|
||||
)} given.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const iterationCount =
|
||||
conf && typeof conf.iterationCount === 'number' ? conf.iterationCount : 1
|
||||
if (!$env.isCore) {
|
||||
if (
|
||||
!(Number.isInteger(iterationCount) && iterationCount > 0) &&
|
||||
iterationCount !== Infinity
|
||||
) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.iterationCount in sequence.play(conf) must be an integer larger than 0. ${JSON.stringify(
|
||||
iterationCount,
|
||||
)} given.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const rate = conf && typeof conf.rate !== 'undefined' ? conf.rate : 1
|
||||
|
||||
if (!$env.isCore) {
|
||||
if (typeof rate !== 'number' || rate === 0) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.rate in sequence.play(conf) must be a number larger than 0. ${JSON.stringify(
|
||||
rate,
|
||||
)} given.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (rate < 0) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.rate in sequence.play(conf) must be a number larger than 0. ${JSON.stringify(
|
||||
rate,
|
||||
)} given. If you want the animation to play backwards, try setting conf.direction to 'reverse' or 'alternateReverse'.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const direction = conf && conf.direction ? conf.direction : 'normal'
|
||||
|
||||
if (!$env.isCore) {
|
||||
if (possibleDirections.indexOf(direction) === -1) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument conf.direction in sequence.play(conf) must be one of ${JSON.stringify(
|
||||
possibleDirections,
|
||||
)}. ${JSON.stringify(direction)} given. ${didYouMean(
|
||||
direction,
|
||||
possibleDirections,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return await this._play(
|
||||
iterationCount,
|
||||
{start: range.start, end: range.end},
|
||||
rate,
|
||||
direction,
|
||||
)
|
||||
}
|
||||
|
||||
protected _play(
|
||||
iterationCount: number,
|
||||
range: IPlaybackRange,
|
||||
rate: number,
|
||||
direction: IPlaybackDirection,
|
||||
): Promise<boolean> {
|
||||
return this._playbackControllerBox
|
||||
.get()
|
||||
.play(iterationCount, range, rate, direction)
|
||||
}
|
||||
|
||||
pause() {
|
||||
this._playbackControllerBox.get().pause()
|
||||
}
|
||||
|
||||
replacePlaybackController(playbackController: IPlaybackController) {
|
||||
this.pause()
|
||||
const oldController = this._playbackControllerBox.get()
|
||||
this._playbackControllerBox.set(playbackController)
|
||||
|
||||
const time = oldController.getCurrentPosition()
|
||||
oldController.destroy()
|
||||
playbackController.gotoPosition(time)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISequencePositionFormatter {
|
||||
formatSubUnitForGrid(posInUnitSpace: number): string
|
||||
formatFullUnitForGrid(posInUnitSpace: number): string
|
||||
formatForPlayhead(posInUnitSpace: number): string
|
||||
}
|
||||
|
||||
class TimeBasedPositionFormatter implements ISequencePositionFormatter {
|
||||
constructor(private readonly _fps: number) {}
|
||||
formatSubUnitForGrid(posInUnitSpace: number): string {
|
||||
const subSecondPos = posInUnitSpace % 1
|
||||
const frame = 1 / this._fps
|
||||
|
||||
const frames = Math.round(subSecondPos / frame)
|
||||
return frames + 'f'
|
||||
}
|
||||
|
||||
formatFullUnitForGrid(posInUnitSpace: number): string {
|
||||
let p = posInUnitSpace
|
||||
|
||||
let s = ''
|
||||
|
||||
if (p >= hour) {
|
||||
const hours = Math.floor(p / hour)
|
||||
s += hours + 'h'
|
||||
p = p % hour
|
||||
}
|
||||
|
||||
if (p >= minute) {
|
||||
const minutes = Math.floor(p / minute)
|
||||
s += minutes + 'm'
|
||||
p = p % minute
|
||||
}
|
||||
|
||||
if (p >= second) {
|
||||
const seconds = Math.floor(p / second)
|
||||
s += seconds + 's'
|
||||
p = p % second
|
||||
}
|
||||
|
||||
const frame = 1 / this._fps
|
||||
|
||||
if (p >= frame) {
|
||||
const frames = Math.floor(p / frame)
|
||||
s += frames + 'f'
|
||||
p = p % frame
|
||||
}
|
||||
|
||||
return s.length === 0 ? '0s' : s
|
||||
}
|
||||
|
||||
formatForPlayhead(posInUnitSpace: number): string {
|
||||
let p = posInUnitSpace
|
||||
|
||||
let s = ''
|
||||
|
||||
if (p >= hour) {
|
||||
const hours = Math.floor(p / hour)
|
||||
s += padStart(hours.toString(), 2, '0') + 'h'
|
||||
p = p % hour
|
||||
}
|
||||
|
||||
if (p >= minute) {
|
||||
const minutes = Math.floor(p / minute)
|
||||
s += padStart(minutes.toString(), 2, '0') + 'm'
|
||||
p = p % minute
|
||||
} else if (s.length > 0) {
|
||||
s += '00m'
|
||||
}
|
||||
|
||||
if (p >= second) {
|
||||
const seconds = Math.floor(p / second)
|
||||
s += padStart(seconds.toString(), 2, '0') + 's'
|
||||
p = p % second
|
||||
} else {
|
||||
s += '00s'
|
||||
}
|
||||
|
||||
const frameLength = 1 / this._fps
|
||||
|
||||
if (p >= frameLength) {
|
||||
const frames = Math.round(p / frameLength)
|
||||
s += padStart(frames.toString(), 2, '0') + 'f'
|
||||
p = p % frameLength
|
||||
} else if (p / frameLength > 0.98) {
|
||||
const frames = 1
|
||||
s += padStart(frames.toString(), 2, '0') + 'f'
|
||||
p = p % frameLength
|
||||
} else {
|
||||
s += '00f'
|
||||
}
|
||||
|
||||
return s.length === 0 ? '00s00f' : s
|
||||
}
|
||||
}
|
||||
|
||||
const second = 1
|
||||
const minute = second * 60
|
||||
const hour = minute * 60
|
76
theatre/core/src/sequences/TheatreSequence.ts
Normal file
76
theatre/core/src/sequences/TheatreSequence.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import logger from '@theatre/shared/logger'
|
||||
import {privateAPI, setPrivateAPI} from '@theatre/shared/privateAPIs'
|
||||
import {defer} from '@theatre/shared/utils/defer'
|
||||
import type Sequence from './Sequence'
|
||||
import type {IPlaybackDirection, IPlaybackRange} from './Sequence'
|
||||
|
||||
export interface ISequence {
|
||||
/**
|
||||
* Starts playback of a sequence.
|
||||
* Returns a promise that either resolves to true when the playback completes,
|
||||
* or resolves to false if playback gets interrupted (for example by calling sequence.pause())
|
||||
*/
|
||||
play(
|
||||
conf?: Partial<{
|
||||
iterationCount: number
|
||||
range: IPlaybackRange
|
||||
rate: number
|
||||
direction: IPlaybackDirection
|
||||
}>,
|
||||
): Promise<boolean>
|
||||
|
||||
pause(): void
|
||||
|
||||
time: number
|
||||
}
|
||||
|
||||
export default class TheatreSequence {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(sheet: Sequence) {
|
||||
setPrivateAPI(this, sheet)
|
||||
}
|
||||
|
||||
play(
|
||||
conf?: Partial<{
|
||||
iterationCount: number
|
||||
range: IPlaybackRange
|
||||
rate: number
|
||||
direction: IPlaybackDirection
|
||||
}>,
|
||||
): Promise<boolean> {
|
||||
if (privateAPI(this)._project.isReady()) {
|
||||
return privateAPI(this).play(conf)
|
||||
} else {
|
||||
if (!$env.isCore) {
|
||||
logger.warn(
|
||||
`You seem to have called sequence.play() before the project has finished loading.\n` +
|
||||
`This would **not** a problem in production when using '@theatre/core', since Theatre loads instantly in core mode. ` +
|
||||
`However, when using '@theatre/studio', it takes a few milliseconds for it to load your project's state, ` +
|
||||
`before which your sequences cannot start playing.\n` +
|
||||
`\n` +
|
||||
`To fix this, simply defer calling sequence.play() until after the project is loaded, like this:\n` +
|
||||
`project.ready.then(() => {\n` +
|
||||
` sequence.play()\n` +
|
||||
`})`,
|
||||
)
|
||||
}
|
||||
const d = defer<boolean>()
|
||||
d.resolve(true)
|
||||
return d.promise
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
privateAPI(this).pause()
|
||||
}
|
||||
|
||||
get time() {
|
||||
return privateAPI(this).position
|
||||
}
|
||||
|
||||
set time(t: number) {
|
||||
privateAPI(this).position = t
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
import type {
|
||||
IPlaybackDirection,
|
||||
IPlaybackRange,
|
||||
} from '@theatre/core/sequences/Sequence'
|
||||
import {defer} from '@theatre/shared/utils/defer'
|
||||
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
|
||||
import noop from '@theatre/shared/utils/noop'
|
||||
import type {Pointer, Ticker} from '@theatre/dataverse'
|
||||
import {Atom} from '@theatre/dataverse'
|
||||
import type {
|
||||
IPlaybackController,
|
||||
IPlaybackState,
|
||||
} from './DefaultPlaybackController'
|
||||
|
||||
export default class AudioPlaybackController implements IPlaybackController {
|
||||
_mainGain: GainNode
|
||||
private _state: Atom<IPlaybackState> = new Atom({position: 0})
|
||||
readonly statePointer: Pointer<IPlaybackState>
|
||||
_stopPlayCallback: () => void = noop
|
||||
playing: boolean = false
|
||||
|
||||
constructor(
|
||||
private readonly _ticker: Ticker,
|
||||
private readonly _decodedBuffer: AudioBuffer,
|
||||
private readonly _audioContext: AudioContext,
|
||||
private readonly _nodeDestination: AudioDestinationNode,
|
||||
) {
|
||||
this.statePointer = this._state.pointer
|
||||
|
||||
this._mainGain = this._audioContext.createGain()
|
||||
this._mainGain.connect(this._nodeDestination)
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
pause() {
|
||||
this._stopPlayCallback()
|
||||
this.playing = false
|
||||
this._stopPlayCallback = noop
|
||||
}
|
||||
|
||||
gotoPosition(time: number) {
|
||||
this._updatePositionInState(time)
|
||||
}
|
||||
|
||||
private _updatePositionInState(time: number) {
|
||||
this._state.reduceState(['position'], () => time)
|
||||
}
|
||||
|
||||
getCurrentPosition() {
|
||||
return this._state.getState().position
|
||||
}
|
||||
|
||||
play(
|
||||
iterationCount: number,
|
||||
range: IPlaybackRange,
|
||||
rate: number,
|
||||
direction: IPlaybackDirection,
|
||||
): Promise<boolean> {
|
||||
if (this.playing) {
|
||||
this.pause()
|
||||
}
|
||||
|
||||
this.playing = true
|
||||
|
||||
const ticker = this._ticker
|
||||
let lastTickerTime = ticker.time
|
||||
const dur = range.end - range.start
|
||||
const prevTime = this.getCurrentPosition()
|
||||
|
||||
if (rate !== 1.0) {
|
||||
throw new InvalidArgumentError(
|
||||
`Audio-controlled sequences can only have a playbackRate of 1.0. ${rate} given.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (direction !== 'normal') {
|
||||
throw new InvalidArgumentError(
|
||||
`Audio-controlled sequences can only be played in the "normal" direction. ` +
|
||||
`'${direction}' given.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (prevTime < range.start || prevTime > range.end) {
|
||||
// if we're currently out of the range
|
||||
this._updatePositionInState(range.start)
|
||||
} else if (prevTime === range.end) {
|
||||
// if we're currently at the very end of the range
|
||||
this._updatePositionInState(range.start)
|
||||
}
|
||||
|
||||
let countSoFar = 1
|
||||
|
||||
const deferred = defer<boolean>()
|
||||
|
||||
const currentSource = this._audioContext.createBufferSource()
|
||||
currentSource.buffer = this._decodedBuffer
|
||||
currentSource.connect(this._mainGain)
|
||||
const audioStartTimeInSeconds = this._audioContext.currentTime
|
||||
const wait = 0
|
||||
const timeToRangeEnd = range.end - prevTime
|
||||
|
||||
if (iterationCount > 1) {
|
||||
currentSource.loop = true
|
||||
currentSource.loopStart = range.start
|
||||
currentSource.loopEnd = range.end
|
||||
}
|
||||
|
||||
currentSource.start(
|
||||
audioStartTimeInSeconds + wait,
|
||||
prevTime - wait,
|
||||
iterationCount === 1 ? wait + timeToRangeEnd : undefined,
|
||||
)
|
||||
|
||||
const tick = (tickerTime: number) => {
|
||||
const lastTime = this.getCurrentPosition()
|
||||
const timeDiff = (tickerTime - lastTickerTime) * rate
|
||||
lastTickerTime = tickerTime
|
||||
/*
|
||||
* I don't know why exactly this happens, but every 10 times or so, the first sequence.play({iterationCount: 1}),
|
||||
* the first call of tick() will have a timeDiff < 0.
|
||||
* This might be because of Spectre mitigation (they randomize performance.now() a bit), or it could be that
|
||||
* I'm using performance.now() the wrong way.
|
||||
* Anyway, this seems like a working fix for it:
|
||||
*/
|
||||
if (timeDiff < 0) {
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
const newTime = lastTime + timeDiff
|
||||
|
||||
if (newTime < range.start) {
|
||||
if (countSoFar === iterationCount) {
|
||||
this._updatePositionInState(range.start)
|
||||
this.playing = false
|
||||
cleanup()
|
||||
deferred.resolve(true)
|
||||
return
|
||||
} else {
|
||||
countSoFar++
|
||||
const diff = (range.start - newTime) % dur
|
||||
|
||||
this._updatePositionInState(range.start + diff)
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
} else if (newTime === range.end) {
|
||||
this._updatePositionInState(range.end)
|
||||
if (countSoFar === iterationCount) {
|
||||
this.playing = false
|
||||
cleanup()
|
||||
deferred.resolve(true)
|
||||
return
|
||||
}
|
||||
requestNextTick()
|
||||
return
|
||||
} else if (newTime > range.end) {
|
||||
if (countSoFar === iterationCount) {
|
||||
this._updatePositionInState(range.end)
|
||||
this.playing = false
|
||||
cleanup()
|
||||
deferred.resolve(true)
|
||||
return
|
||||
} else {
|
||||
countSoFar++
|
||||
const diff = (newTime - range.end) % dur
|
||||
this._updatePositionInState(range.start + diff)
|
||||
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
this._updatePositionInState(newTime)
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
currentSource.stop()
|
||||
currentSource.disconnect()
|
||||
}
|
||||
|
||||
this._stopPlayCallback = () => {
|
||||
cleanup()
|
||||
ticker.offThisOrNextTick(tick)
|
||||
ticker.offNextTick(tick)
|
||||
|
||||
if (this.playing) deferred.resolve(false)
|
||||
}
|
||||
const requestNextTick = () => ticker.onNextTick(tick)
|
||||
ticker.onThisOrNextTick(tick)
|
||||
return deferred.promise
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
import type {
|
||||
IPlaybackDirection,
|
||||
IPlaybackRange,
|
||||
} from '@theatre/core/sequences/Sequence'
|
||||
import {defer} from '@theatre/shared/utils/defer'
|
||||
import noop from '@theatre/shared/utils/noop'
|
||||
import type {Pointer, Ticker} from '@theatre/dataverse'
|
||||
import {Atom} from '@theatre/dataverse'
|
||||
|
||||
export interface IPlaybackState {
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface IPlaybackController {
|
||||
playing: boolean
|
||||
getCurrentPosition(): number
|
||||
gotoPosition(position: number): void
|
||||
statePointer: Pointer<IPlaybackState>
|
||||
destroy(): void
|
||||
|
||||
play(
|
||||
iterationCount: number,
|
||||
range: IPlaybackRange,
|
||||
rate: number,
|
||||
direction: IPlaybackDirection,
|
||||
): Promise<boolean>
|
||||
|
||||
pause(): void
|
||||
}
|
||||
|
||||
export default class DefaultPlaybackController implements IPlaybackController {
|
||||
playing: boolean = false
|
||||
_stopPlayCallback: () => void = noop
|
||||
private _state: Atom<IPlaybackState> = new Atom({position: 0})
|
||||
readonly statePointer: Pointer<IPlaybackState>
|
||||
constructor(private readonly _ticker: Ticker) {
|
||||
this.statePointer = this._state.pointer
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
pause() {
|
||||
this._stopPlayCallback()
|
||||
this.playing = false
|
||||
this._stopPlayCallback = noop
|
||||
}
|
||||
|
||||
gotoPosition(time: number) {
|
||||
this._updatePositionInState(time)
|
||||
}
|
||||
|
||||
private _updatePositionInState(time: number) {
|
||||
this._state.reduceState(['position'], () => time)
|
||||
}
|
||||
|
||||
getCurrentPosition() {
|
||||
return this._state.getState().position
|
||||
}
|
||||
|
||||
play(
|
||||
iterationCount: number,
|
||||
range: IPlaybackRange,
|
||||
rate: number,
|
||||
direction: IPlaybackDirection,
|
||||
): Promise<boolean> {
|
||||
if (this.playing) {
|
||||
this.pause()
|
||||
}
|
||||
|
||||
this.playing = true
|
||||
|
||||
const ticker = this._ticker
|
||||
let lastTickerTime = ticker.time
|
||||
const dur = range.end - range.start
|
||||
const prevTime = this.getCurrentPosition()
|
||||
|
||||
if (prevTime < range.start || prevTime > range.end) {
|
||||
this._updatePositionInState(range.start)
|
||||
} else if (
|
||||
prevTime === range.end &&
|
||||
(direction === 'normal' || direction === 'alternate')
|
||||
) {
|
||||
this._updatePositionInState(range.start)
|
||||
} else if (
|
||||
prevTime === range.start &&
|
||||
(direction === 'reverse' || direction === 'alternateReverse')
|
||||
) {
|
||||
this._updatePositionInState(range.end)
|
||||
}
|
||||
|
||||
let goingForward =
|
||||
direction === 'alternateReverse' || direction === 'reverse' ? -1 : 1
|
||||
|
||||
let countSoFar = 1
|
||||
|
||||
const deferred = defer<boolean>()
|
||||
|
||||
const tick = (tickerTimeInMs: number) => {
|
||||
const tickerTime = tickerTimeInMs / 1000
|
||||
const lastTime = this.getCurrentPosition()
|
||||
const timeDiff = (tickerTime - lastTickerTime) * (rate * goingForward)
|
||||
lastTickerTime = tickerTime
|
||||
/*
|
||||
* I don't know why exactly this happens, but every 10 times or so, the first sequence.play({iterationCount: 1}),
|
||||
* the first call of tick() will have a timeDiff < 0.
|
||||
* This might be because of Spectre mitigation (they randomize performance.now() a bit), or it could be that
|
||||
* I'm using performance.now() the wrong way.
|
||||
* Anyway, this seems like a working fix for it:
|
||||
*/
|
||||
if (timeDiff < 0) {
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
const newTime = lastTime + timeDiff
|
||||
|
||||
if (newTime < range.start) {
|
||||
if (countSoFar === iterationCount) {
|
||||
this._updatePositionInState(range.start)
|
||||
this.playing = false
|
||||
deferred.resolve(true)
|
||||
return
|
||||
} else {
|
||||
countSoFar++
|
||||
const diff = (range.start - newTime) % dur
|
||||
if (direction === 'reverse') {
|
||||
this._updatePositionInState(range.end - diff)
|
||||
} else {
|
||||
goingForward = 1
|
||||
this._updatePositionInState(range.start + diff)
|
||||
}
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
} else if (newTime === range.end) {
|
||||
this._updatePositionInState(range.end)
|
||||
if (countSoFar === iterationCount) {
|
||||
this.playing = false
|
||||
deferred.resolve(true)
|
||||
return
|
||||
}
|
||||
requestNextTick()
|
||||
return
|
||||
} else if (newTime > range.end) {
|
||||
if (countSoFar === iterationCount) {
|
||||
this._updatePositionInState(range.end)
|
||||
this.playing = false
|
||||
deferred.resolve(true)
|
||||
return
|
||||
} else {
|
||||
countSoFar++
|
||||
const diff = (newTime - range.end) % dur
|
||||
if (direction === 'normal') {
|
||||
this._updatePositionInState(range.start + diff)
|
||||
} else {
|
||||
goingForward = -1
|
||||
this._updatePositionInState(range.end - diff)
|
||||
}
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
this._updatePositionInState(newTime)
|
||||
requestNextTick()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this._stopPlayCallback = () => {
|
||||
ticker.offThisOrNextTick(tick)
|
||||
ticker.offNextTick(tick)
|
||||
|
||||
if (this.playing) deferred.resolve(false)
|
||||
}
|
||||
const requestNextTick = () => ticker.onNextTick(tick)
|
||||
ticker.onThisOrNextTick(tick)
|
||||
return deferred.promise
|
||||
}
|
||||
}
|
201
theatre/core/src/sequences/trackValueAtTime.ts
Normal file
201
theatre/core/src/sequences/trackValueAtTime.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
import type {
|
||||
BasicKeyframedTrack,
|
||||
Keyframe,
|
||||
TrackData,
|
||||
} from '@theatre/core/projects/store/types/SheetState_Historic'
|
||||
import type {IDerivation, Pointer} from '@theatre/dataverse'
|
||||
import {ConstantDerivation, prism, val} from '@theatre/dataverse'
|
||||
import logger from '@theatre/shared/logger'
|
||||
import UnitBezier from 'timing-function/lib/UnitBezier'
|
||||
|
||||
export default function trackValueAtTime(
|
||||
trackP: Pointer<TrackData | undefined>,
|
||||
timeD: IDerivation<number>,
|
||||
): IDerivation<unknown> {
|
||||
return prism(() => {
|
||||
const track = val(trackP)
|
||||
const driverD = prism.memo(
|
||||
'driver',
|
||||
() => {
|
||||
if (!track) {
|
||||
return new ConstantDerivation(undefined)
|
||||
} else if (track.type === 'BasicKeyframedTrack') {
|
||||
return trackValueAtTime_basicKeyframedTrack(track, timeD)
|
||||
} else {
|
||||
logger.error(`Track type not yet supported.`)
|
||||
return new ConstantDerivation(undefined)
|
||||
}
|
||||
},
|
||||
[track],
|
||||
)
|
||||
|
||||
return driverD.getValue()
|
||||
})
|
||||
}
|
||||
|
||||
type IStartedState = {
|
||||
started: true
|
||||
validFrom: number
|
||||
validTo: number
|
||||
der: IDerivation<unknown>
|
||||
}
|
||||
type IState = {started: false} | IStartedState
|
||||
|
||||
function trackValueAtTime_basicKeyframedTrack(
|
||||
track: BasicKeyframedTrack,
|
||||
timeD: IDerivation<number>,
|
||||
): IDerivation<unknown> {
|
||||
return prism(() => {
|
||||
let stateRef = prism.ref<IState>('state', {started: false})
|
||||
let state = stateRef.current
|
||||
|
||||
const time = timeD.getValue()
|
||||
|
||||
if (!state.started || time < state.validFrom || state.validTo <= time) {
|
||||
stateRef.current = state = pp(timeD, track)
|
||||
}
|
||||
|
||||
return state.der.getValue()
|
||||
})
|
||||
}
|
||||
|
||||
const undefinedConstD = new ConstantDerivation(undefined)
|
||||
|
||||
const pp = (
|
||||
progressionD: IDerivation<number>,
|
||||
track: BasicKeyframedTrack,
|
||||
): IStartedState => {
|
||||
const progression = progressionD.getValue()
|
||||
if (track.keyframes.length === 0) {
|
||||
return {
|
||||
started: true,
|
||||
validFrom: -Infinity,
|
||||
validTo: Infinity,
|
||||
der: undefinedConstD,
|
||||
}
|
||||
}
|
||||
|
||||
let currentKeyframeIndex = 0
|
||||
|
||||
while (true) {
|
||||
const currentKeyframe = track.keyframes[currentKeyframeIndex]
|
||||
|
||||
if (!currentKeyframe) {
|
||||
if ($env.NODE_ENV === 'development') {
|
||||
logger.error(`Bug here`)
|
||||
}
|
||||
return states.error
|
||||
}
|
||||
|
||||
const isLastKeyframe = currentKeyframeIndex === track.keyframes.length - 1
|
||||
|
||||
if (progression < currentKeyframe.position) {
|
||||
if (currentKeyframeIndex === 0) {
|
||||
return states.beforeFirstKeyframe(currentKeyframe)
|
||||
} else {
|
||||
if ($env.NODE_ENV === 'development') {
|
||||
logger.error(`Bug here`)
|
||||
}
|
||||
return states.error
|
||||
// note: uncomment these if we support starting with currentPointIndex != 0
|
||||
// currentPointIndex--
|
||||
// continue
|
||||
}
|
||||
} else if (currentKeyframe.position === progression) {
|
||||
if (isLastKeyframe) {
|
||||
return states.lastKeyframe(currentKeyframe)
|
||||
} else {
|
||||
return states.between(
|
||||
currentKeyframe,
|
||||
track.keyframes[currentKeyframeIndex + 1],
|
||||
progressionD,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// last point
|
||||
if (currentKeyframeIndex === track.keyframes.length - 1) {
|
||||
return states.lastKeyframe(currentKeyframe)
|
||||
} else {
|
||||
const nextKeyframeIndex = currentKeyframeIndex + 1
|
||||
if (track.keyframes[nextKeyframeIndex].position <= progression) {
|
||||
currentKeyframeIndex = nextKeyframeIndex
|
||||
continue
|
||||
} else {
|
||||
return states.between(
|
||||
currentKeyframe,
|
||||
track.keyframes[currentKeyframeIndex + 1],
|
||||
progressionD,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const states = {
|
||||
beforeFirstKeyframe(kf: Keyframe): IStartedState {
|
||||
return {
|
||||
started: true,
|
||||
validFrom: -Infinity,
|
||||
validTo: kf.position,
|
||||
der: new ConstantDerivation(kf.value),
|
||||
}
|
||||
},
|
||||
lastKeyframe(kf: Keyframe): IStartedState {
|
||||
return {
|
||||
started: true,
|
||||
validFrom: kf.position,
|
||||
validTo: Infinity,
|
||||
der: new ConstantDerivation(kf.value),
|
||||
}
|
||||
},
|
||||
between(
|
||||
left: Keyframe,
|
||||
right: Keyframe,
|
||||
progressionD: IDerivation<number>,
|
||||
): IStartedState {
|
||||
if (!left.connectedRight) {
|
||||
return {
|
||||
started: true,
|
||||
validFrom: left.position,
|
||||
validTo: right.position,
|
||||
der: new ConstantDerivation(left.value),
|
||||
}
|
||||
}
|
||||
|
||||
const solver = new UnitBezier(
|
||||
left.handles[2],
|
||||
left.handles[3],
|
||||
right.handles[0],
|
||||
right.handles[1],
|
||||
)
|
||||
const globalProgressionToLocalProgression = (
|
||||
globalProgression: number,
|
||||
): number => {
|
||||
return (
|
||||
(globalProgression - left.position) / (right.position - left.position)
|
||||
)
|
||||
}
|
||||
const der = prism(() => {
|
||||
const progression = globalProgressionToLocalProgression(
|
||||
progressionD.getValue(),
|
||||
)
|
||||
|
||||
const valueProgression = solver.solveSimple(progression)
|
||||
return left.value + valueProgression * (right.value - left.value)
|
||||
})
|
||||
|
||||
return {
|
||||
started: true,
|
||||
validFrom: left.position,
|
||||
validTo: right.position,
|
||||
der,
|
||||
}
|
||||
},
|
||||
error: {
|
||||
started: true,
|
||||
validFrom: -Infinity,
|
||||
validTo: Infinity,
|
||||
der: undefinedConstD,
|
||||
} as IStartedState,
|
||||
}
|
131
theatre/core/src/sheetObjects/SheetObject.test.ts
Normal file
131
theatre/core/src/sheetObjects/SheetObject.test.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import {setupTestSheet} from '@theatre/shared/testUtils'
|
||||
import {encodePathToProp} from '@theatre/shared/utils/addresses'
|
||||
import {asKeyframeId, asSequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import {iterateOver, prism, val} from '@theatre/dataverse'
|
||||
|
||||
describe(`SheetObject`, () => {
|
||||
test('it should support setting/unsetting static props', async () => {
|
||||
const {obj, studio} = await setupTestSheet({
|
||||
staticOverrides: {
|
||||
byObject: {
|
||||
obj: {
|
||||
position: {
|
||||
x: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const objValues = iterateOver(
|
||||
prism(() => {
|
||||
return val(val(obj.getValues()))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 10, y: 1, z: 2},
|
||||
})
|
||||
|
||||
// setting a static
|
||||
studio.transaction(({set}) => {
|
||||
set(obj.propsP.position.y, 5)
|
||||
})
|
||||
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 10, y: 5, z: 2},
|
||||
})
|
||||
|
||||
// unsetting a static
|
||||
studio.transaction(({unset}) => {
|
||||
unset(obj.propsP.position.y)
|
||||
})
|
||||
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 10, y: 1, z: 2},
|
||||
})
|
||||
|
||||
objValues.return()
|
||||
})
|
||||
|
||||
test('it should support sequenced props', async () => {
|
||||
const {obj, sheet} = await setupTestSheet({
|
||||
staticOverrides: {
|
||||
byObject: {},
|
||||
},
|
||||
sequence: {
|
||||
type: 'PositionalSequence',
|
||||
length: 20,
|
||||
subUnitsPerUnit: 30,
|
||||
tracksByObject: {
|
||||
obj: {
|
||||
trackIdByPropPath: {
|
||||
[encodePathToProp(['position', 'y'])]: asSequenceTrackId('1'),
|
||||
},
|
||||
trackData: {
|
||||
'1': {
|
||||
type: 'BasicKeyframedTrack',
|
||||
keyframes: [
|
||||
{
|
||||
id: asKeyframeId('0'),
|
||||
position: 10,
|
||||
connectedRight: true,
|
||||
handles: [0.5, 0.5, 0.5, 0.5],
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
id: asKeyframeId('1'),
|
||||
position: 20,
|
||||
connectedRight: false,
|
||||
handles: [0.5, 0.5, 0.5, 0.5],
|
||||
value: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const seq = sheet.publicApi.sequence()
|
||||
|
||||
const objValues = iterateOver(
|
||||
prism(() => {
|
||||
return val(val(obj.getValues()))
|
||||
}),
|
||||
)
|
||||
|
||||
expect(seq.time).toEqual(0)
|
||||
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 3, z: 2},
|
||||
})
|
||||
|
||||
seq.time = 5
|
||||
expect(seq.time).toEqual(5)
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 3, z: 2},
|
||||
})
|
||||
|
||||
seq.time = 11
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 3.29999747758308, z: 2},
|
||||
})
|
||||
|
||||
seq.time = 15
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 4.5, z: 2},
|
||||
})
|
||||
|
||||
seq.time = 22
|
||||
expect(objValues.next().value).toMatchObject({
|
||||
position: {x: 0, y: 6, z: 2},
|
||||
})
|
||||
|
||||
objValues.return()
|
||||
})
|
||||
})
|
203
theatre/core/src/sheetObjects/SheetObject.ts
Normal file
203
theatre/core/src/sheetObjects/SheetObject.ts
Normal file
|
@ -0,0 +1,203 @@
|
|||
import trackValueAtTime from '@theatre/core/sequences/trackValueAtTime'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet'
|
||||
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
||||
import deepMergeWithCache from '@theatre/shared/utils/deepMergeWithCache'
|
||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import pointerDeep from '@theatre/shared/utils/pointerDeep'
|
||||
import SimpleCache from '@theatre/shared/utils/SimpleCache'
|
||||
import type {
|
||||
$FixMe,
|
||||
$IntentionalAny,
|
||||
SerializableMap,
|
||||
SerializableValue,
|
||||
} from '@theatre/shared/utils/types'
|
||||
import {valToAtom} from '@theatre/shared/utils/valToAtom'
|
||||
import type {IDerivation, Pointer} from '@theatre/dataverse'
|
||||
import {Atom, getPointerParts, pointer, prism, val} from '@theatre/dataverse'
|
||||
import type SheetObjectTemplate from './SheetObjectTemplate'
|
||||
import TheatreSheetObject from './TheatreSheetObject'
|
||||
|
||||
// type Everything = {
|
||||
// final: SerializableMap
|
||||
// statics: SerializableMap
|
||||
// defaults: SerializableMap
|
||||
// sequenced: SerializableMap
|
||||
// }
|
||||
|
||||
export default class SheetObject {
|
||||
get type(): 'Theatre_SheetObject' {
|
||||
return 'Theatre_SheetObject'
|
||||
}
|
||||
|
||||
readonly address: SheetObjectAddress
|
||||
readonly publicApi: TheatreSheetObject<$IntentionalAny>
|
||||
private readonly _initialValue = new Atom<SerializableMap>({})
|
||||
private readonly _cache = new SimpleCache()
|
||||
|
||||
constructor(
|
||||
readonly sheet: Sheet,
|
||||
readonly template: SheetObjectTemplate,
|
||||
readonly nativeObject: unknown,
|
||||
) {
|
||||
this.address = {
|
||||
...template.address,
|
||||
sheetInstanceId: sheet.address.sheetInstanceId,
|
||||
}
|
||||
|
||||
this.publicApi = new TheatreSheetObject(this)
|
||||
}
|
||||
|
||||
overrideConfig(
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<$IntentionalAny>,
|
||||
) {
|
||||
if (nativeObject !== this.nativeObject) {
|
||||
// @todo
|
||||
}
|
||||
|
||||
this.template.overrideConfig(nativeObject, config)
|
||||
}
|
||||
|
||||
// getValues(): Record<string, unknown> {
|
||||
// return {}
|
||||
// }
|
||||
|
||||
getValues(): IDerivation<Pointer<SerializableMap>> {
|
||||
return this._cache.get('getValues()', () =>
|
||||
prism(() => {
|
||||
const defaults = val(this.template.getDefaultValues())
|
||||
|
||||
const initial = val(this._initialValue.pointer)
|
||||
|
||||
const withInitialCache = prism.memo(
|
||||
'withInitialCache',
|
||||
() => new WeakMap(),
|
||||
[],
|
||||
)
|
||||
|
||||
const withInitial = deepMergeWithCache(
|
||||
defaults,
|
||||
initial,
|
||||
withInitialCache,
|
||||
)
|
||||
|
||||
const statics = val(this.template.getStaticValues())
|
||||
|
||||
const withStaticsCache = prism.memo(
|
||||
'withStatics',
|
||||
() => new WeakMap(),
|
||||
[],
|
||||
)
|
||||
|
||||
const withStatics = deepMergeWithCache(
|
||||
withInitial,
|
||||
statics,
|
||||
withStaticsCache,
|
||||
)
|
||||
|
||||
let final = withStatics
|
||||
let sequenced
|
||||
|
||||
{
|
||||
const pointerToSequencedValuesD = prism.memo(
|
||||
'seq',
|
||||
() => this.getSequencedValues(),
|
||||
[],
|
||||
)
|
||||
const withSeqsCache = prism.memo(
|
||||
'withSeqsCache',
|
||||
() => new WeakMap(),
|
||||
[],
|
||||
)
|
||||
sequenced = val(val(pointerToSequencedValuesD))
|
||||
|
||||
const withSeqs = deepMergeWithCache(final, sequenced, withSeqsCache)
|
||||
final = withSeqs
|
||||
}
|
||||
|
||||
const a = valToAtom<SerializableMap>('finalAtom', final)
|
||||
|
||||
return a.pointer
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
getValueByPointer(pointer: SheetObject['propsP']): SerializableValue {
|
||||
const allValuesP = val(this.getValues())
|
||||
const {path} = getPointerParts(pointer)
|
||||
|
||||
return val(pointerDeep(allValuesP as $FixMe, path)) as SerializableMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns values of props that are sequenced.
|
||||
*/
|
||||
getSequencedValues(): IDerivation<Pointer<SerializableMap>> {
|
||||
return prism(() => {
|
||||
const tracksToProcessD = prism.memo(
|
||||
'tracksToProcess',
|
||||
() => this.template.getArrayOfValidSequenceTracks(),
|
||||
[],
|
||||
)
|
||||
|
||||
const tracksToProcess = val(tracksToProcessD)
|
||||
const valsAtom = new Atom<SerializableMap>({})
|
||||
|
||||
prism.effect(
|
||||
'processTracks',
|
||||
() => {
|
||||
const untaps: Array<() => void> = []
|
||||
|
||||
for (const {trackId, pathToProp} of tracksToProcess) {
|
||||
const derivation = this._trackIdToDerivation(trackId)
|
||||
|
||||
const updateSequenceValueFromItsDerivation = () => {
|
||||
valsAtom.setIn(pathToProp, derivation.getValue())
|
||||
}
|
||||
const untap = derivation
|
||||
.changesWithoutValues()
|
||||
.tap(updateSequenceValueFromItsDerivation)
|
||||
|
||||
updateSequenceValueFromItsDerivation()
|
||||
untaps.push(untap)
|
||||
}
|
||||
return () => {
|
||||
for (const untap of untaps) {
|
||||
untap()
|
||||
}
|
||||
}
|
||||
},
|
||||
tracksToProcess,
|
||||
)
|
||||
|
||||
return valsAtom.pointer
|
||||
})
|
||||
}
|
||||
|
||||
protected _trackIdToDerivation(
|
||||
trackId: SequenceTrackId,
|
||||
): IDerivation<unknown> {
|
||||
const trackP =
|
||||
this.template.project.pointers.historic.sheetsById[this.address.sheetId]
|
||||
.sequence.tracksByObject[this.address.objectKey].trackData[trackId]
|
||||
const timeD = this.sheet.getSequence().positionDerivation
|
||||
|
||||
return trackValueAtTime(trackP, timeD)
|
||||
}
|
||||
|
||||
get propsP(): Pointer<$FixMe> {
|
||||
return this._cache.get('propsP', () =>
|
||||
pointer<{props: {}}>({root: this, path: []}),
|
||||
) as $FixMe
|
||||
}
|
||||
|
||||
validateValue(pointer: Pointer<$FixMe>, value: unknown) {
|
||||
// @todo
|
||||
}
|
||||
|
||||
setInitialValue(val: SerializableMap) {
|
||||
this.validateValue(this.propsP, val)
|
||||
this._initialValue.setState(val)
|
||||
}
|
||||
}
|
106
theatre/core/src/sheetObjects/SheetObjectTemplate.test.ts
Normal file
106
theatre/core/src/sheetObjects/SheetObjectTemplate.test.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import {setupTestSheet} from '@theatre/shared/testUtils'
|
||||
import {encodePathToProp} from '@theatre/shared/utils/addresses'
|
||||
import {asSequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import {iterateOver} from '@theatre/dataverse'
|
||||
|
||||
describe(`SheetObjectTemplate`, () => {
|
||||
describe(`getArrayOfValidSequenceTracks()`, () => {
|
||||
it('should only include valid tracks', async () => {
|
||||
const {obj} = await setupTestSheet({
|
||||
staticOverrides: {
|
||||
byObject: {},
|
||||
},
|
||||
sequence: {
|
||||
type: 'PositionalSequence',
|
||||
subUnitsPerUnit: 30,
|
||||
length: 10,
|
||||
tracksByObject: {
|
||||
obj: {
|
||||
trackIdByPropPath: {
|
||||
[encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'),
|
||||
[encodePathToProp(['position', 'invalid'])]: asSequenceTrackId(
|
||||
'invalidTrack',
|
||||
),
|
||||
},
|
||||
trackData: {
|
||||
x: null as $IntentionalAny,
|
||||
invalid: null as $IntentionalAny,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const iter = iterateOver(obj.template.getArrayOfValidSequenceTracks())
|
||||
|
||||
const validTracks = iter.next().value
|
||||
expect(validTracks).toHaveLength(1)
|
||||
expect(validTracks).toMatchObject([
|
||||
{
|
||||
pathToProp: ['position', 'x'],
|
||||
trackId: 'x',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should return empty array when no tracks are set up', async () => {
|
||||
const {obj} = await setupTestSheet({
|
||||
staticOverrides: {
|
||||
byObject: {},
|
||||
},
|
||||
sequence: {
|
||||
type: 'PositionalSequence',
|
||||
subUnitsPerUnit: 30,
|
||||
length: 10,
|
||||
tracksByObject: {},
|
||||
},
|
||||
})
|
||||
const iter = iterateOver(obj.template.getArrayOfValidSequenceTracks())
|
||||
|
||||
expect(iter.next().value).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
describe(`getMapOfValidSequenceTracks_forStudio()`, () => {
|
||||
it('should return valid sequences in map form', async () => {
|
||||
const {obj} = await setupTestSheet({
|
||||
staticOverrides: {
|
||||
byObject: {},
|
||||
},
|
||||
sequence: {
|
||||
type: 'PositionalSequence',
|
||||
subUnitsPerUnit: 30,
|
||||
length: 10,
|
||||
tracksByObject: {
|
||||
obj: {
|
||||
trackIdByPropPath: {
|
||||
[encodePathToProp(['position', 'x'])]: asSequenceTrackId('x'),
|
||||
[encodePathToProp(['position', 'invalid'])]: asSequenceTrackId(
|
||||
'invalidTrack',
|
||||
),
|
||||
},
|
||||
trackData: {
|
||||
x: null as $IntentionalAny,
|
||||
invalid: null as $IntentionalAny,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const iter = iterateOver(
|
||||
obj.template.getMapOfValidSequenceTracks_forStudio(),
|
||||
)
|
||||
|
||||
const validTracks = iter.next().value
|
||||
expect(validTracks).toMatchObject({
|
||||
position: {
|
||||
x: 'x',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
209
theatre/core/src/sheetObjects/SheetObjectTemplate.ts
Normal file
209
theatre/core/src/sheetObjects/SheetObjectTemplate.ts
Normal file
|
@ -0,0 +1,209 @@
|
|||
import type Project from '@theatre/core/projects/Project'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import type SheetTemplate from '@theatre/core/sheets/SheetTemplate'
|
||||
import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet'
|
||||
import {emptyArray} from '@theatre/shared/utils'
|
||||
import type {
|
||||
PathToProp,
|
||||
SheetObjectAddress,
|
||||
WithoutSheetInstance,
|
||||
} from '@theatre/shared/utils/addresses'
|
||||
import getDeep from '@theatre/shared/utils/getDeep'
|
||||
import type {SequenceTrackId} from '@theatre/shared/utils/ids'
|
||||
import SimpleCache from '@theatre/shared/utils/SimpleCache'
|
||||
import type {
|
||||
$FixMe,
|
||||
$IntentionalAny,
|
||||
SerializableMap,
|
||||
SerializableValue,
|
||||
} from '@theatre/shared/utils/types'
|
||||
import type {IDerivation, Pointer} from '@theatre/dataverse'
|
||||
import {
|
||||
Atom,
|
||||
ConstantDerivation,
|
||||
getPointerParts,
|
||||
prism,
|
||||
val,
|
||||
} from '@theatre/dataverse'
|
||||
import get from 'lodash-es/get'
|
||||
import set from 'lodash-es/set'
|
||||
import getPropDefaultsOfSheetObject from './getPropDefaultsOfSheetObject'
|
||||
import SheetObject from './SheetObject'
|
||||
import logger from '@theatre/shared/logger'
|
||||
|
||||
export type IPropPathToTrackIdTree = {
|
||||
[key in string]?: SequenceTrackId | IPropPathToTrackIdTree
|
||||
}
|
||||
|
||||
export default class SheetObjectTemplate {
|
||||
readonly address: WithoutSheetInstance<SheetObjectAddress>
|
||||
readonly type: 'Theatre_SheetObjectTemplate' = 'Theatre_SheetObjectTemplate'
|
||||
protected _config: Atom<SheetObjectConfig<$IntentionalAny>>
|
||||
readonly _cache = new SimpleCache()
|
||||
readonly project: Project
|
||||
|
||||
get config() {
|
||||
return this._config.getState()
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly sheetTemplate: SheetTemplate,
|
||||
objectKey: string,
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<$IntentionalAny>,
|
||||
) {
|
||||
this.address = {...sheetTemplate.address, objectKey}
|
||||
this._config = new Atom(config)
|
||||
this.project = sheetTemplate.project
|
||||
}
|
||||
|
||||
createInstance(
|
||||
sheet: Sheet,
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<$IntentionalAny>,
|
||||
): SheetObject {
|
||||
this._config.setState(config)
|
||||
return new SheetObject(sheet, this, nativeObject)
|
||||
}
|
||||
|
||||
overrideConfig(
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<$IntentionalAny>,
|
||||
) {
|
||||
this._config.setState(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default values (all defaults are read from the config)
|
||||
*/
|
||||
getDefaultValues(): IDerivation<SerializableMap> {
|
||||
return this._cache.get('getDefaultValues()', () =>
|
||||
prism(() => {
|
||||
const config = val(this._config.pointer)
|
||||
return getPropDefaultsOfSheetObject(config)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns values that are set statically (ie, not sequenced, and not defaults)
|
||||
*/
|
||||
getStaticValues(): IDerivation<SerializableMap> {
|
||||
return this._cache.get('getDerivationOfStatics', () =>
|
||||
prism(() => {
|
||||
const pointerToSheetState =
|
||||
this.sheetTemplate.project.pointers.historic.sheetsById[
|
||||
this.address.sheetId
|
||||
]
|
||||
|
||||
return (
|
||||
val(
|
||||
pointerToSheetState.staticOverrides.byObject[
|
||||
this.address.objectKey
|
||||
],
|
||||
) || {}
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters through the sequenced tracks those tracks who are valid
|
||||
* according to the object's prop types.
|
||||
*
|
||||
* Returns an array.
|
||||
*/
|
||||
getArrayOfValidSequenceTracks(): IDerivation<
|
||||
Array<{pathToProp: PathToProp; trackId: SequenceTrackId}>
|
||||
> {
|
||||
return this._cache.get('getArrayOfValidSequenceTracks', () =>
|
||||
prism((): Array<{pathToProp: PathToProp; trackId: SequenceTrackId}> => {
|
||||
const defaults = val(this.getDefaultValues())
|
||||
|
||||
const pointerToSheetState =
|
||||
this.project.pointers.historic.sheetsById[this.address.sheetId]
|
||||
|
||||
const trackIdByPropPath = val(
|
||||
pointerToSheetState.sequence.tracksByObject[this.address.objectKey]
|
||||
.trackIdByPropPath,
|
||||
)
|
||||
|
||||
const arrayOfIds: Array<{
|
||||
pathToProp: PathToProp
|
||||
trackId: SequenceTrackId
|
||||
}> = []
|
||||
|
||||
if (trackIdByPropPath) {
|
||||
for (const [pathToPropInString, trackId] of Object.entries(
|
||||
trackIdByPropPath,
|
||||
)) {
|
||||
let pathToProp
|
||||
try {
|
||||
pathToProp = JSON.parse(pathToPropInString)
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`property ${JSON.stringify(
|
||||
pathToPropInString,
|
||||
)} cannot be parsed. Skipping.`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
const defaultValue = get(defaults, pathToProp)
|
||||
if (
|
||||
typeof defaultValue === 'undefined' ||
|
||||
typeof defaultValue === 'object'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
arrayOfIds.push({pathToProp, trackId: trackId!})
|
||||
}
|
||||
} else {
|
||||
return emptyArray as $IntentionalAny
|
||||
}
|
||||
|
||||
if (arrayOfIds.length === 0) {
|
||||
return emptyArray as $IntentionalAny
|
||||
} else {
|
||||
return arrayOfIds
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters through the sequenced tracks those tracks that are valid
|
||||
* according to the object's prop types.
|
||||
*
|
||||
* Returns a map.
|
||||
*
|
||||
* Not available in core.
|
||||
*/
|
||||
getMapOfValidSequenceTracks_forStudio(): IDerivation<IPropPathToTrackIdTree> {
|
||||
if (!$env.isCore) {
|
||||
return this._cache.get('getMapOfValidSequenceTracks_forStudio', () =>
|
||||
this.getArrayOfValidSequenceTracks().map((arr) => {
|
||||
let map = {}
|
||||
|
||||
for (const {pathToProp, trackId} of arr) {
|
||||
set(map, pathToProp, trackId)
|
||||
}
|
||||
|
||||
return map
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
return new ConstantDerivation({})
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultsAtPointer(
|
||||
pointer: Pointer<unknown>,
|
||||
): SerializableValue | undefined {
|
||||
const {path} = getPointerParts(pointer)
|
||||
const defaults = this.getDefaultValues().getValue()
|
||||
|
||||
const defaultsAtPath = getDeep(defaults, path)
|
||||
return defaultsAtPath as $FixMe
|
||||
}
|
||||
}
|
90
theatre/core/src/sheetObjects/TheatreSheetObject.ts
Normal file
90
theatre/core/src/sheetObjects/TheatreSheetObject.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import {privateAPI, setPrivateAPI} from '@theatre/shared/privateAPIs'
|
||||
import type {IProject} from '@theatre/core/projects/TheatreProject'
|
||||
import coreTicker from '@theatre/core/coreTicker'
|
||||
import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
|
||||
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
|
||||
import SimpleCache from '@theatre/shared/utils/SimpleCache'
|
||||
import type {
|
||||
$FixMe,
|
||||
$IntentionalAny,
|
||||
DeepPartialOfSerializableValue,
|
||||
VoidFn,
|
||||
} from '@theatre/shared/utils/types'
|
||||
import type {IDerivation, Pointer} from '@theatre/dataverse'
|
||||
import {prism, val} from '@theatre/dataverse'
|
||||
import type {PropTypeConfig_Compound} from '@theatre/shared/src/propTypes'
|
||||
import type SheetObject from './SheetObject'
|
||||
|
||||
export interface ISheetObject<
|
||||
Props extends PropTypeConfig_Compound<$IntentionalAny> = PropTypeConfig_Compound<$IntentionalAny>,
|
||||
> {
|
||||
readonly type: 'Theatre_SheetObject_PublicAPI'
|
||||
/**
|
||||
* The type of the values of the SheetObject.
|
||||
*/
|
||||
readonly value: Props['valueType']
|
||||
readonly props: Pointer<Props['valueType']>
|
||||
|
||||
readonly sheet: ISheet
|
||||
readonly project: IProject
|
||||
readonly address: SheetObjectAddress
|
||||
|
||||
onValuesChange(fn: (values: Props['valueType']) => void): VoidFn
|
||||
// prettier-ignore
|
||||
set initialValue(value: DeepPartialOfSerializableValue<Props['valueType']>)
|
||||
}
|
||||
|
||||
export default class TheatreSheetObject<
|
||||
Props extends PropTypeConfig_Compound<$IntentionalAny>,
|
||||
> implements ISheetObject<Props>
|
||||
{
|
||||
get type(): 'Theatre_SheetObject_PublicAPI' {
|
||||
return 'Theatre_SheetObject_PublicAPI'
|
||||
}
|
||||
private readonly _cache = new SimpleCache()
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(internals: SheetObject) {
|
||||
setPrivateAPI(this, internals)
|
||||
}
|
||||
|
||||
get props(): Pointer<Props['valueType']> {
|
||||
return privateAPI(this).propsP as $FixMe
|
||||
}
|
||||
|
||||
get sheet(): ISheet {
|
||||
return privateAPI(this).sheet.publicApi
|
||||
}
|
||||
|
||||
get project(): IProject {
|
||||
return privateAPI(this).sheet.project.publicApi
|
||||
}
|
||||
|
||||
get address(): SheetObjectAddress {
|
||||
return {...privateAPI(this).address}
|
||||
}
|
||||
|
||||
private _valuesDerivation(): IDerivation<Props['valueType']> {
|
||||
return this._cache.get('onValuesChangeDerivation', () => {
|
||||
const sheetObject = privateAPI(this)
|
||||
const d: IDerivation<Props> = prism(() => {
|
||||
return val(sheetObject.getValues().getValue()) as $FixMe
|
||||
})
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
onValuesChange(fn: (values: Props['valueType']) => void): VoidFn {
|
||||
return this._valuesDerivation().tapImmediate(coreTicker, fn)
|
||||
}
|
||||
|
||||
get value(): Props['valueType'] {
|
||||
return this._valuesDerivation().getValue()
|
||||
}
|
||||
|
||||
set initialValue(val: DeepPartialOfSerializableValue<Props['valueType']>) {
|
||||
privateAPI(this).setInitialValue(val)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet'
|
||||
import type {
|
||||
$FixMe,
|
||||
$IntentionalAny,
|
||||
SerializableMap,
|
||||
SerializableValue,
|
||||
} from '@theatre/shared/utils/types'
|
||||
import type {
|
||||
PropTypeConfig,
|
||||
PropTypeConfig_Compound,
|
||||
PropTypeConfig_Enum,
|
||||
} from '@theatre/shared/src/propTypes'
|
||||
|
||||
const cachedDefaults = new WeakMap<PropTypeConfig, SerializableValue>()
|
||||
|
||||
/**
|
||||
* Generates and caches a default value for the config of a SheetObject.
|
||||
*/
|
||||
export default function getPropDefaultsOfSheetObject(
|
||||
config: SheetObjectConfig<$IntentionalAny>,
|
||||
): SerializableMap {
|
||||
return getDefaultsOfPropTypeConfig(config.props) as $IntentionalAny
|
||||
}
|
||||
|
||||
function getDefaultsOfPropTypeConfig(
|
||||
config: PropTypeConfig,
|
||||
): SerializableValue {
|
||||
if (cachedDefaults.has(config)) {
|
||||
return cachedDefaults.get(config)!
|
||||
}
|
||||
|
||||
const generated =
|
||||
config.type === 'compound'
|
||||
? generateDefaultsForCompound(config)
|
||||
: config.type === 'enum'
|
||||
? generateDefaultsForEnum(config)
|
||||
: config.default
|
||||
|
||||
cachedDefaults.set(config, generated)
|
||||
|
||||
return generated
|
||||
}
|
||||
|
||||
function generateDefaultsForEnum(config: PropTypeConfig_Enum) {
|
||||
const defaults: SerializableMap = {
|
||||
$case: config.defaultCase,
|
||||
}
|
||||
|
||||
for (const [case_, caseConf] of Object.entries(config.cases)) {
|
||||
defaults[case_] = getDefaultsOfPropTypeConfig(caseConf)
|
||||
}
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
function generateDefaultsForCompound(config: PropTypeConfig_Compound<$FixMe>) {
|
||||
const defaults: SerializableMap = {}
|
||||
for (const [key, propConf] of Object.entries(config.props)) {
|
||||
defaults[key] = getDefaultsOfPropTypeConfig(propConf)
|
||||
}
|
||||
|
||||
return defaults
|
||||
}
|
82
theatre/core/src/sheets/Sheet.ts
Normal file
82
theatre/core/src/sheets/Sheet.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import type Project from '@theatre/core/projects/Project'
|
||||
import Sequence from '@theatre/core/sequences/Sequence'
|
||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||
import type {SheetObjectConfig} from '@theatre/core/sheets/TheatreSheet'
|
||||
import TheatreSheet from '@theatre/core/sheets/TheatreSheet'
|
||||
import type {SheetAddress} from '@theatre/shared/utils/addresses'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import {Atom, valueDerivation} from '@theatre/dataverse'
|
||||
import type SheetTemplate from './SheetTemplate'
|
||||
|
||||
type IObjects = {[key: string]: SheetObject}
|
||||
|
||||
export default class Sheet {
|
||||
private readonly _objects: Atom<IObjects> = new Atom<IObjects>({})
|
||||
private _sequence: undefined | Sequence
|
||||
readonly address: SheetAddress
|
||||
readonly publicApi: TheatreSheet
|
||||
readonly project: Project
|
||||
readonly objectsP = this._objects.pointer
|
||||
type: 'Theatre_Sheet' = 'Theatre_Sheet'
|
||||
|
||||
constructor(
|
||||
readonly template: SheetTemplate,
|
||||
public readonly instanceId: string,
|
||||
) {
|
||||
this.project = template.project
|
||||
this.address = {
|
||||
...template.address,
|
||||
sheetInstanceId: this.instanceId,
|
||||
}
|
||||
|
||||
this.publicApi = new TheatreSheet(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @remarks At some point, we have to reconcile the concept of "an object"
|
||||
* with that of "an element."
|
||||
*/
|
||||
createObject(
|
||||
key: string,
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<$IntentionalAny>,
|
||||
): SheetObject {
|
||||
const objTemplate = this.template.getObjectTemplate(
|
||||
key,
|
||||
nativeObject,
|
||||
config,
|
||||
)
|
||||
|
||||
const object = objTemplate.createInstance(this, nativeObject, config)
|
||||
|
||||
this._objects.setIn([key], object)
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
getObject(key: string): SheetObject | undefined {
|
||||
return this._objects.getState()[key]
|
||||
}
|
||||
|
||||
getSequence(): Sequence {
|
||||
if (!this._sequence) {
|
||||
const lengthD = valueDerivation(
|
||||
this.project.pointers.historic.sheetsById[this.address.sheetId].sequence
|
||||
.length,
|
||||
).map((s) => (typeof s === 'number' ? s : 10))
|
||||
|
||||
const subUnitsPerUnitD = valueDerivation(
|
||||
this.project.pointers.historic.sheetsById[this.address.sheetId].sequence
|
||||
.subUnitsPerUnit,
|
||||
).map((s) => (typeof s === 'number' ? s : 30))
|
||||
|
||||
this._sequence = new Sequence(
|
||||
this.template.project,
|
||||
this,
|
||||
lengthD,
|
||||
subUnitsPerUnitD,
|
||||
)
|
||||
}
|
||||
return this._sequence
|
||||
}
|
||||
}
|
52
theatre/core/src/sheets/SheetTemplate.ts
Normal file
52
theatre/core/src/sheets/SheetTemplate.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import type Project from '@theatre/core/projects/Project'
|
||||
import SheetObjectTemplate from '@theatre/core/sheetObjects/SheetObjectTemplate'
|
||||
import type {
|
||||
SheetAddress,
|
||||
WithoutSheetInstance,
|
||||
} from '@theatre/shared/utils/addresses'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import {Atom} from '@theatre/dataverse'
|
||||
import Sheet from './Sheet'
|
||||
import type {SheetObjectConfig} from './TheatreSheet'
|
||||
|
||||
export default class SheetTemplate {
|
||||
readonly type: 'Theatre_SheetTemplate' = 'Theatre_SheetTemplate'
|
||||
readonly address: WithoutSheetInstance<SheetAddress>
|
||||
private _instances = new Atom<{[instanceId: string]: Sheet}>({})
|
||||
readonly instancesP = this._instances.pointer
|
||||
|
||||
private _objectTemplates = new Atom<{
|
||||
[objectKey: string]: SheetObjectTemplate
|
||||
}>({})
|
||||
readonly objectTemplatesP = this._objectTemplates.pointer
|
||||
|
||||
constructor(readonly project: Project, sheetId: string) {
|
||||
this.address = {...project.address, sheetId}
|
||||
}
|
||||
|
||||
getInstance(instanceId: string): Sheet {
|
||||
let inst = this._instances.getState()[instanceId]
|
||||
|
||||
if (!inst) {
|
||||
inst = new Sheet(this, instanceId)
|
||||
this._instances.setIn([instanceId], inst)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
getObjectTemplate(
|
||||
key: string,
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<$IntentionalAny>,
|
||||
): SheetObjectTemplate {
|
||||
let template = this._objectTemplates.getState()[key]
|
||||
|
||||
if (!template) {
|
||||
template = new SheetObjectTemplate(this, key, nativeObject, config)
|
||||
this._objectTemplates.setIn([key], template)
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
}
|
103
theatre/core/src/sheets/TheatreSheet.ts
Normal file
103
theatre/core/src/sheets/TheatreSheet.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import {privateAPI, setPrivateAPI} from '@theatre/shared/privateAPIs'
|
||||
import type {IProject} from '@theatre/core/projects/TheatreProject'
|
||||
import type TheatreSequence from '@theatre/core/sequences/TheatreSequence'
|
||||
import type {ISequence} from '@theatre/core/sequences/TheatreSequence'
|
||||
import type {PropTypeConfig_Compound} from '@theatre/shared/src/propTypes'
|
||||
import type {ISheetObject} from '@theatre/core/sheetObjects/TheatreSheetObject'
|
||||
import type Sheet from '@theatre/core/sheets/Sheet'
|
||||
import type {SheetAddress} from '@theatre/shared/utils/addresses'
|
||||
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
|
||||
import {validateAndSanitiseSlashedPathOrThrow} from '@theatre/shared/utils/slashedPaths'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
|
||||
|
||||
export type SheetObjectConfig<
|
||||
Props extends PropTypeConfig_Compound<$IntentionalAny>,
|
||||
> = {
|
||||
props: Props
|
||||
}
|
||||
|
||||
export interface ISheet {
|
||||
readonly type: 'Theatre_Sheet_PublicAPI'
|
||||
readonly project: IProject
|
||||
readonly address: SheetAddress
|
||||
|
||||
object<Props extends PropTypeConfig_Compound<$IntentionalAny>>(
|
||||
key: string,
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<Props>,
|
||||
): ISheetObject<Props>
|
||||
|
||||
sequence(): ISequence
|
||||
}
|
||||
|
||||
export default class TheatreSheet implements ISheet {
|
||||
get type(): 'Theatre_Sheet_PublicAPI' {
|
||||
return 'Theatre_Sheet_PublicAPI'
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(sheet: Sheet) {
|
||||
setPrivateAPI(this, sheet)
|
||||
}
|
||||
|
||||
object<Props extends PropTypeConfig_Compound<$IntentionalAny>>(
|
||||
key: string,
|
||||
nativeObject: unknown,
|
||||
config: SheetObjectConfig<Props>,
|
||||
): ISheetObject<Props> {
|
||||
const internal = privateAPI(this)
|
||||
const sanitizedPath = validateAndSanitiseSlashedPathOrThrow(
|
||||
key,
|
||||
`sheet.object("${key}", ...)`,
|
||||
)
|
||||
|
||||
// @todo sanitize config
|
||||
|
||||
const existingObject = internal.getObject(sanitizedPath)
|
||||
|
||||
if (existingObject) {
|
||||
existingObject.overrideConfig(nativeObject, config)
|
||||
return existingObject.publicApi as $IntentionalAny
|
||||
} else {
|
||||
const object = internal.createObject(sanitizedPath, nativeObject, config)
|
||||
return object.publicApi as $IntentionalAny
|
||||
}
|
||||
}
|
||||
|
||||
sequence(): TheatreSequence {
|
||||
return privateAPI(this).getSequence().publicApi
|
||||
}
|
||||
|
||||
get project(): IProject {
|
||||
return privateAPI(this).project.publicApi
|
||||
}
|
||||
|
||||
get address(): SheetAddress {
|
||||
return {...privateAPI(this).address}
|
||||
}
|
||||
}
|
||||
|
||||
const validateSequenceNameOrThrow = (value: string) => {
|
||||
if (typeof value !== 'string') {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument 'name' in \`sheet.getSequence(name)\` must be a string. Instead, it was ${userReadableTypeOfValue(
|
||||
value,
|
||||
)}.`,
|
||||
)
|
||||
}
|
||||
|
||||
const idTrimmed = value.trim()
|
||||
if (idTrimmed.length !== value.length) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument 'name' in \`sheet.getSequence("${value}")\` should not have surrounding whitespace.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (idTrimmed.length < 3) {
|
||||
throw new InvalidArgumentError(
|
||||
`Argument 'name' in \`sheet.getSequence("${value}")\` should be at least 3 characters long.`,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue