fragment-tools 0.2.10 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/src/cli/createConfig.js +37 -18
- package/src/cli/plugins/hot-shader-replacement.js +0 -5
- package/src/cli/run.js +27 -6
- package/src/cli/templates/blank/index.ts +1 -1
- package/src/cli/templates/default/index.js +10 -2
- package/src/cli/templates/default/index.ts +5 -2
- package/src/cli/templates/fragment-gl/index.ts +1 -1
- package/src/cli/templates/p5/index.ts +1 -1
- package/src/cli/templates/p5-webgl/index.ts +1 -1
- package/src/cli/templates/three-fragment/index.js +5 -3
- package/src/cli/templates/three-fragment/index.ts +5 -4
- package/src/cli/templates/three-orthographic/index.js +6 -1
- package/src/cli/templates/three-orthographic/index.ts +6 -2
- package/src/cli/templates/three-perspective/index.js +6 -1
- package/src/cli/templates/three-perspective/index.ts +6 -2
- package/src/client/app/modules/Params.svelte +1 -0
- package/src/client/app/renderers/THREERenderer.js +3 -2
- package/src/client/app/state/Sketch.svelte.js +1 -1
- package/src/client/app/state/rendering.svelte.js +44 -14
- package/src/client/app/ui/Field.svelte +58 -16
- package/src/client/app/ui/ParamsOutput.svelte +6 -4
- package/src/client/app/ui/SketchRenderer.svelte +5 -0
- package/src/client/app/ui/fields/TextareaInput.svelte +93 -0
- package/src/client/app/utils/fields.utils.js +13 -2
- package/src/index.js +12 -0
- package/src/types/config.d.ts +6 -0
- package/src/types/index.d.ts +1 -0
- package/src/types/props.d.ts +33 -5
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fragment-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "A web development environment for creative coding",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"fragment": "bin/index.js"
|
|
8
8
|
},
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
"convert-length": "^1.0.1",
|
|
31
31
|
"get-port": "^7.1.0",
|
|
32
32
|
"gifenc": "^1.0.3",
|
|
33
|
-
"glslify": "^7.1.1",
|
|
34
33
|
"is-unicode-supported": "^2.0.0",
|
|
35
34
|
"kleur": "^4.1.4",
|
|
36
35
|
"mediabunny": "^1.13.3",
|
package/src/cli/createConfig.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import url from 'node:url';
|
|
3
4
|
import { defineConfig, loadConfigFromFile, mergeConfig } from 'vite';
|
|
4
5
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
5
6
|
|
|
@@ -8,30 +9,47 @@ import { __dirname, file } from './utils.js';
|
|
|
8
9
|
import { log } from './log.js';
|
|
9
10
|
import sketches from './plugins/sketches.js';
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {{ cwd: string, filepath: string | undefined }} params
|
|
15
|
+
* @returns {Promise<import('../types/config.js').Config>}
|
|
16
|
+
*/
|
|
11
17
|
export async function loadConfig({ cwd, filepath }) {
|
|
12
18
|
try {
|
|
13
|
-
let filename = `fragment.config.js`;
|
|
14
|
-
let configFile = filepath ? filepath : filename;
|
|
15
19
|
let configRoot = cwd;
|
|
16
|
-
let
|
|
20
|
+
let filenames = [`fragment.config.js`, `fragment.config.ts`];
|
|
17
21
|
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
log.error(`Config file not found: ${resolvedPath}`);
|
|
21
|
-
}
|
|
22
|
-
return {};
|
|
22
|
+
if (filepath) {
|
|
23
|
+
filenames = [filepath, ...filenames];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
let filepaths = filenames.map((filename) => {
|
|
27
|
+
return path.resolve(configRoot, filename);
|
|
28
|
+
});
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
command: 'build',
|
|
30
|
-
mode: 'dev',
|
|
31
|
-
},
|
|
32
|
-
configFile,
|
|
33
|
-
configRoot,
|
|
30
|
+
let resolvedIndex = filepaths.findIndex((filepath) =>
|
|
31
|
+
fs.existsSync(filepath),
|
|
34
32
|
);
|
|
33
|
+
/** @type {string|undefined} */
|
|
34
|
+
let resolvedPath = filepaths[resolvedIndex];
|
|
35
|
+
|
|
36
|
+
if (filepath && resolvedIndex !== 0) {
|
|
37
|
+
log.error(`Config file not found: ${filepath}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!resolvedPath) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let configFile = path.relative(cwd, resolvedPath);
|
|
45
|
+
|
|
46
|
+
log.info(`Extending configuration from ${configFile}`);
|
|
47
|
+
|
|
48
|
+
const config = (
|
|
49
|
+
await import(
|
|
50
|
+
`${url.pathToFileURL(configFile).href}?ts=${Date.now()}`
|
|
51
|
+
)
|
|
52
|
+
).default;
|
|
35
53
|
|
|
36
54
|
return config;
|
|
37
55
|
} catch (error) {
|
|
@@ -43,9 +61,10 @@ export async function loadConfig({ cwd, filepath }) {
|
|
|
43
61
|
/**
|
|
44
62
|
* Create Vite config from entries
|
|
45
63
|
* @param {string[]} entries
|
|
46
|
-
* @param {
|
|
64
|
+
* @param {object} [options]
|
|
47
65
|
* @param {boolean} [options.dev=false]
|
|
48
66
|
* @param {boolean} [options.build=false]
|
|
67
|
+
* @param {string} [configFilepath]
|
|
49
68
|
* @param {string} [cwd=process.cwd()]
|
|
50
69
|
* @returns {import('vite').UserConfig}
|
|
51
70
|
*/
|
|
@@ -63,8 +82,8 @@ export async function createConfig(
|
|
|
63
82
|
log.info(`Creating Vite configuration...`);
|
|
64
83
|
|
|
65
84
|
const config = await loadConfig({
|
|
66
|
-
filepath: configFilepath,
|
|
67
85
|
cwd,
|
|
86
|
+
filepath: configFilepath,
|
|
68
87
|
});
|
|
69
88
|
|
|
70
89
|
return mergeConfig(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import glslify from 'glslify';
|
|
5
4
|
import { log, dim, green, yellow } from '../log.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -232,10 +231,6 @@ ${keyword}${shaderParts[1]}
|
|
|
232
231
|
shaderPath,
|
|
233
232
|
);
|
|
234
233
|
|
|
235
|
-
code = glslify(code, {
|
|
236
|
-
basedir: process.cwd(),
|
|
237
|
-
});
|
|
238
|
-
|
|
239
234
|
if (server) {
|
|
240
235
|
code = addShaderFilepath(code, shaderPath);
|
|
241
236
|
}
|
package/src/cli/run.js
CHANGED
|
@@ -26,17 +26,23 @@ import hotShaderReplacement from './plugins/hot-shader-replacement.js';
|
|
|
26
26
|
*/
|
|
27
27
|
export async function run(entry, options = {}) {
|
|
28
28
|
let fragmentServer;
|
|
29
|
+
/** @type {import('node:fs').FSWatcher} */
|
|
30
|
+
let watcher;
|
|
29
31
|
|
|
30
32
|
const cwd = process.cwd();
|
|
31
33
|
const command = `run`;
|
|
32
34
|
const prefix = log.prefix(command);
|
|
35
|
+
|
|
36
|
+
const stop = () => {
|
|
37
|
+
fragmentServer?.close();
|
|
38
|
+
watcher?.close();
|
|
39
|
+
};
|
|
40
|
+
|
|
33
41
|
const exit = () => {
|
|
34
42
|
process.off('SIGTERM', exit);
|
|
35
43
|
process.off('exit', exit);
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
fragmentServer.close();
|
|
39
|
-
}
|
|
45
|
+
stop();
|
|
40
46
|
|
|
41
47
|
console.log();
|
|
42
48
|
};
|
|
@@ -62,9 +68,10 @@ export async function run(entry, options = {}) {
|
|
|
62
68
|
);
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
const hasTSFiles = entries.some((entry) => entry.endsWith('ts'));
|
|
65
72
|
const tsConfigDirpath = path.join(cwd, FRAGMENT_DIRECTORY);
|
|
66
73
|
const tsConfigFilepath = path.join(tsConfigDirpath, 'tsconfig.json');
|
|
67
|
-
if (!fs.existsSync(tsConfigFilepath)) {
|
|
74
|
+
if (!fs.existsSync(tsConfigFilepath) && hasTSFiles) {
|
|
68
75
|
await createTsConfigFile(cwd);
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -106,6 +113,22 @@ export async function run(entry, options = {}) {
|
|
|
106
113
|
],
|
|
107
114
|
}),
|
|
108
115
|
);
|
|
116
|
+
|
|
117
|
+
watcher = fs.watch(cwd, (eventType, filename) => {
|
|
118
|
+
if (
|
|
119
|
+
['fragment.config.js', 'fragment.config.ts'].includes(
|
|
120
|
+
filename,
|
|
121
|
+
) ||
|
|
122
|
+
options.configFilepath?.includes(filename)
|
|
123
|
+
) {
|
|
124
|
+
log.warn(`${filename} has changed. Restarting...`);
|
|
125
|
+
console.log();
|
|
126
|
+
server.close();
|
|
127
|
+
stop();
|
|
128
|
+
run(entry, options);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
109
132
|
await server.listen();
|
|
110
133
|
|
|
111
134
|
// line break after logs
|
|
@@ -130,8 +153,6 @@ export async function run(entry, options = {}) {
|
|
|
130
153
|
|
|
131
154
|
// line break before fragment logs
|
|
132
155
|
log.message();
|
|
133
|
-
|
|
134
|
-
return server;
|
|
135
156
|
} catch (error) {
|
|
136
157
|
// line break before error
|
|
137
158
|
log.message();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
let resolution = { x: 0, y: 0 };
|
|
2
|
+
|
|
1
3
|
export const props = {};
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -24,8 +26,11 @@ export const init = ({ canvas, context, width, height }) => {};
|
|
|
24
26
|
* @param {number} params.playcount
|
|
25
27
|
*/
|
|
26
28
|
export const update = ({ context, width, height, pixelRatio }) => {
|
|
29
|
+
const w = width * pixelRatio;
|
|
30
|
+
const h = height * pixelRatio;
|
|
31
|
+
|
|
27
32
|
context.fillStyle = 'rgb(0, 255, 0)';
|
|
28
|
-
context.fillRect(0, 0,
|
|
33
|
+
context.fillRect(0, 0, w, h);
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
/**
|
|
@@ -35,6 +40,9 @@ export const update = ({ context, width, height, pixelRatio }) => {
|
|
|
35
40
|
* @param {number} params.height
|
|
36
41
|
* @param {number} params.pixelRatio
|
|
37
42
|
*/
|
|
38
|
-
export const resize = ({ width, height }) => {
|
|
43
|
+
export const resize = ({ width, height, pixelRatio }) => {
|
|
44
|
+
resolution.x = width * pixelRatio;
|
|
45
|
+
resolution.y = height * pixelRatio;
|
|
46
|
+
};
|
|
39
47
|
|
|
40
48
|
export const rendering = '2d';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
1
|
+
import type { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
2
2
|
import { defineProps } from '@fragment/types/utils';
|
|
3
3
|
|
|
4
4
|
export const props = defineProps({});
|
|
@@ -11,8 +11,11 @@ export const update: Update<'2d'> = ({
|
|
|
11
11
|
height,
|
|
12
12
|
pixelRatio,
|
|
13
13
|
}) => {
|
|
14
|
+
const w = width * pixelRatio;
|
|
15
|
+
const h = height * pixelRatio;
|
|
16
|
+
|
|
14
17
|
context.fillStyle = 'rgb(0, 255, 0)';
|
|
15
|
-
context.fillRect(0, 0,
|
|
18
|
+
context.fillRect(0, 0, w, h);
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export const resize: Resize<'2d'> = ({}) => {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import p5, { type Shader } from 'p5';
|
|
2
2
|
|
|
3
|
-
import { Init, Rendering, Update } from '@fragment/types';
|
|
3
|
+
import type { Init, Rendering, Update } from '@fragment/types';
|
|
4
4
|
import { defineProps } from '@fragment/types/utils';
|
|
5
5
|
|
|
6
6
|
import fragmentShader from './fragment.fs';
|
|
@@ -6,9 +6,11 @@ import fragmentShader from './fragment.fs';
|
|
|
6
6
|
let scene;
|
|
7
7
|
/** @type {THREE.OrthographicCamera} */
|
|
8
8
|
let camera;
|
|
9
|
+
/** @type {THREE.Vector2} */
|
|
10
|
+
let resolution = new THREE.Vector2();
|
|
9
11
|
|
|
10
12
|
let uniforms = {
|
|
11
|
-
uResolution: { value:
|
|
13
|
+
uResolution: { value: resolution },
|
|
12
14
|
uTime: { value: 0 },
|
|
13
15
|
};
|
|
14
16
|
|
|
@@ -87,8 +89,8 @@ export const update = ({ renderer, time, deltaTime }) => {
|
|
|
87
89
|
* @param {number} params.pixelRatio
|
|
88
90
|
*/
|
|
89
91
|
export const resize = ({ width, height, pixelRatio }) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
resolution.x = width * pixelRatio;
|
|
93
|
+
resolution.y = height * pixelRatio;
|
|
92
94
|
|
|
93
95
|
camera.left = -width * 0.5;
|
|
94
96
|
camera.right = width * 0.5;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
|
-
import { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
3
|
+
import type { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
4
4
|
|
|
5
5
|
import fragmentShader from './fragment.fs';
|
|
6
6
|
|
|
7
7
|
let scene: THREE.Scene;
|
|
8
8
|
let camera: THREE.OrthographicCamera;
|
|
9
|
+
let resolution = new THREE.Vector2();
|
|
9
10
|
let uniforms = {
|
|
10
|
-
uResolution: { value:
|
|
11
|
+
uResolution: { value: resolution },
|
|
11
12
|
uTime: { value: 0 },
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -54,8 +55,8 @@ export const update: Update<'three'> = ({ renderer, time }) => {
|
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
export const resize: Resize<'three'> = ({ width, height, pixelRatio }) => {
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
resolution.x = width * pixelRatio;
|
|
59
|
+
resolution.y = height * pixelRatio;
|
|
59
60
|
|
|
60
61
|
camera.left = -width * 0.5;
|
|
61
62
|
camera.right = width * 0.5;
|
|
@@ -4,6 +4,8 @@ import * as THREE from 'three';
|
|
|
4
4
|
let scene;
|
|
5
5
|
/** @type {THREE.OrthographicCamera} */
|
|
6
6
|
let camera;
|
|
7
|
+
/** @type {THREE.Vector2} */
|
|
8
|
+
let resolution = new THREE.Vector2();
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* @param {object} params
|
|
@@ -49,7 +51,10 @@ export const update = ({ renderer, time, deltaTime }) => {
|
|
|
49
51
|
* @param {number} params.height
|
|
50
52
|
* @param {number} params.pixelRatio
|
|
51
53
|
*/
|
|
52
|
-
export const resize = ({ width, height }) => {
|
|
54
|
+
export const resize = ({ width, height, pixelRatio }) => {
|
|
55
|
+
resolution.x = width * pixelRatio;
|
|
56
|
+
resolution.y = height * pixelRatio;
|
|
57
|
+
|
|
53
58
|
camera.left = -width * 0.5;
|
|
54
59
|
camera.right = width * 0.5;
|
|
55
60
|
camera.top = height * 0.5;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
|
-
import { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
3
|
+
import type { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
4
4
|
|
|
5
5
|
let scene: THREE.Scene;
|
|
6
6
|
let camera: THREE.OrthographicCamera;
|
|
7
|
+
let resolution = new THREE.Vector2();
|
|
7
8
|
|
|
8
9
|
export const init: Init<'three'> = ({}) => {
|
|
9
10
|
scene = new THREE.Scene();
|
|
@@ -17,7 +18,10 @@ export const update: Update<'three'> = ({ renderer }) => {
|
|
|
17
18
|
renderer.render(scene, camera);
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
export const resize: Resize<'three'> = ({ width, height }) => {
|
|
21
|
+
export const resize: Resize<'three'> = ({ width, height, pixelRatio }) => {
|
|
22
|
+
resolution.x = width * pixelRatio;
|
|
23
|
+
resolution.y = height * pixelRatio;
|
|
24
|
+
|
|
21
25
|
camera.left = -width * 0.5;
|
|
22
26
|
camera.right = width * 0.5;
|
|
23
27
|
camera.top = height * 0.5;
|
|
@@ -4,6 +4,8 @@ import * as THREE from 'three';
|
|
|
4
4
|
let scene;
|
|
5
5
|
/** @type {THREE.OrthographicCamera} */
|
|
6
6
|
let camera;
|
|
7
|
+
/** @type {THREE.Vector2} */
|
|
8
|
+
let resolution = new THREE.Vector2();
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* @param {object} params
|
|
@@ -50,7 +52,10 @@ export const update = ({ renderer, time, deltaTime }) => {
|
|
|
50
52
|
* @param {number} params.height
|
|
51
53
|
* @param {number} params.pixelRatio
|
|
52
54
|
*/
|
|
53
|
-
export const resize = ({ width, height }) => {
|
|
55
|
+
export const resize = ({ width, height, pixelRatio }) => {
|
|
56
|
+
resolution.x = width * pixelRatio;
|
|
57
|
+
resolution.y = height * pixelRatio;
|
|
58
|
+
|
|
54
59
|
camera.aspect = width / height;
|
|
55
60
|
camera.updateProjectionMatrix();
|
|
56
61
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
|
-
import { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
3
|
+
import type { Init, Rendering, Resize, Update } from '@fragment/types';
|
|
4
4
|
|
|
5
5
|
let scene: THREE.Scene;
|
|
6
6
|
let camera: THREE.PerspectiveCamera;
|
|
7
|
+
let resolution = new THREE.Vector2();
|
|
7
8
|
|
|
8
9
|
export const init: Init<'three'> = ({}) => {
|
|
9
10
|
scene = new THREE.Scene();
|
|
@@ -18,7 +19,10 @@ export const update: Update<'three'> = ({ renderer }) => {
|
|
|
18
19
|
renderer.render(scene, camera);
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
export const resize: Resize<'three'> = ({ width, height }) => {
|
|
22
|
+
export const resize: Resize<'three'> = ({ width, height, pixelRatio }) => {
|
|
23
|
+
resolution.x = width * pixelRatio;
|
|
24
|
+
resolution.y = height * pixelRatio;
|
|
25
|
+
|
|
22
26
|
camera.aspect = width / height;
|
|
23
27
|
camera.updateProjectionMatrix();
|
|
24
28
|
};
|
|
@@ -14,7 +14,7 @@ import { clearError } from '../state/errors.svelte';
|
|
|
14
14
|
* @typedef {object} PreviewThreeRenderer
|
|
15
15
|
* @property {number} id
|
|
16
16
|
* @property {THREE.Scene} scene
|
|
17
|
-
* @property {THREE.
|
|
17
|
+
* @property {THREE.WebGLrenderer} renderer
|
|
18
18
|
* @property {rendered} boolean
|
|
19
19
|
*/
|
|
20
20
|
|
|
@@ -32,7 +32,7 @@ let previews = [];
|
|
|
32
32
|
* @returns {MountParamsThreeRenderer}
|
|
33
33
|
*/
|
|
34
34
|
export let onMountPreview = ({ id, canvas }) => {
|
|
35
|
-
let renderer = new WebGLRenderer({ antialias: true
|
|
35
|
+
let renderer = new WebGLRenderer({ antialias: true });
|
|
36
36
|
|
|
37
37
|
const render = renderer.render;
|
|
38
38
|
|
|
@@ -125,6 +125,7 @@ export let onDestroyPreview = ({ id }) => {
|
|
|
125
125
|
const { renderer } = preview;
|
|
126
126
|
clearError(renderer.getContext().__uuid);
|
|
127
127
|
renderer.dispose();
|
|
128
|
+
renderer.forceContextLoss();
|
|
128
129
|
previews.splice(previewIndex, 1);
|
|
129
130
|
}
|
|
130
131
|
};
|
|
@@ -481,7 +481,7 @@ class Sketch {
|
|
|
481
481
|
fieldgroups.forEach((fieldgroup) => {
|
|
482
482
|
const hasAllFieldsHidden = fieldgroup.children
|
|
483
483
|
.filter((child) => child.type === 'field')
|
|
484
|
-
.every((child) => this.props[child.key]
|
|
484
|
+
.every((child) => this.props[child.key]?.__hidden());
|
|
485
485
|
const hasAllFieldgroupsHidden = fieldgroup.children
|
|
486
486
|
.filter((child) => child.type === 'fieldgroup')
|
|
487
487
|
.every((child) => child.hidden);
|
|
@@ -94,6 +94,10 @@ class Rendering {
|
|
|
94
94
|
this.estimateRefreshRate();
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
*
|
|
99
|
+
* @param {string} renderingMode
|
|
100
|
+
*/
|
|
97
101
|
loadRenderer(renderingMode) {
|
|
98
102
|
if (__THREE_RENDERER__ && renderingMode === 'three') {
|
|
99
103
|
return import('../renderers/THREERenderer.js');
|
|
@@ -321,7 +325,8 @@ export class Render {
|
|
|
321
325
|
this.time = 0;
|
|
322
326
|
this.recording = false;
|
|
323
327
|
|
|
324
|
-
|
|
328
|
+
/** @type {number|null} */
|
|
329
|
+
let resizeTimeout = null;
|
|
325
330
|
|
|
326
331
|
$effect.pre(() => {
|
|
327
332
|
const { width, height, pixelRatio } = rendering;
|
|
@@ -330,7 +335,10 @@ export class Render {
|
|
|
330
335
|
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
331
336
|
|
|
332
337
|
resizeTimeout = setTimeout(() => {
|
|
333
|
-
|
|
338
|
+
if (resizeTimeout) {
|
|
339
|
+
clearTimeout(resizeTimeout);
|
|
340
|
+
}
|
|
341
|
+
|
|
334
342
|
resizeTimeout = null;
|
|
335
343
|
|
|
336
344
|
this.resize(width, height, pixelRatio);
|
|
@@ -399,6 +407,7 @@ export class Render {
|
|
|
399
407
|
this.then = performance.now();
|
|
400
408
|
|
|
401
409
|
this.playhead = 0;
|
|
410
|
+
this.playheadLast = 0;
|
|
402
411
|
this.playcount = 0;
|
|
403
412
|
this.frame = 0;
|
|
404
413
|
|
|
@@ -443,24 +452,23 @@ export class Render {
|
|
|
443
452
|
return;
|
|
444
453
|
}
|
|
445
454
|
|
|
446
|
-
let
|
|
447
|
-
playhead
|
|
448
|
-
|
|
455
|
+
let totalPlayhead = time / 1000 / duration;
|
|
456
|
+
let playhead = fps === 0 ? 0 : totalPlayhead % 1;
|
|
457
|
+
|
|
458
|
+
if (playhead < this.playheadLast) {
|
|
459
|
+
this.playcount++;
|
|
460
|
+
}
|
|
461
|
+
this.playheadLast = playhead;
|
|
462
|
+
|
|
449
463
|
if (isFinite(interval)) {
|
|
450
464
|
// round values for low framerates
|
|
451
465
|
playhead = Math.floor(playhead / interval) * interval;
|
|
452
466
|
}
|
|
453
|
-
|
|
454
|
-
playcount = Math.floor(this.timeTotal / 1000 / duration);
|
|
455
|
-
}
|
|
456
|
-
if (fps === 0) {
|
|
457
|
-
playhead = 0;
|
|
458
|
-
}
|
|
467
|
+
|
|
459
468
|
const now = performance.now();
|
|
460
469
|
const deltaTime = now - this.thenLoop;
|
|
461
470
|
|
|
462
471
|
this.playhead = playhead;
|
|
463
|
-
this.playcount = playcount;
|
|
464
472
|
this.frame = Math.floor(map(playhead, 0, 1, 1, frameCount + 1));
|
|
465
473
|
if (
|
|
466
474
|
this.elapsed === 0 ||
|
|
@@ -499,6 +507,9 @@ export class Render {
|
|
|
499
507
|
|
|
500
508
|
this.raf = requestAnimationFrame(() => {
|
|
501
509
|
this.time = new Date().getTime() - rendering.today;
|
|
510
|
+
this.initialTime = this.time;
|
|
511
|
+
this.playheadLast = 0;
|
|
512
|
+
this.playcount = 0;
|
|
502
513
|
this.then = this.time;
|
|
503
514
|
this.thenLoop = performance.now();
|
|
504
515
|
this.update(this.time);
|
|
@@ -522,6 +533,14 @@ export class Render {
|
|
|
522
533
|
});
|
|
523
534
|
}
|
|
524
535
|
|
|
536
|
+
/**
|
|
537
|
+
*
|
|
538
|
+
* @param {Object} params
|
|
539
|
+
* @param {HTMLElement} params.container
|
|
540
|
+
* @param {HTMLCanvasElement} [params.canvas]
|
|
541
|
+
* @param {string} params.context
|
|
542
|
+
* @returns
|
|
543
|
+
*/
|
|
525
544
|
createCanvas({
|
|
526
545
|
container,
|
|
527
546
|
canvas = document.createElement('canvas'),
|
|
@@ -543,6 +562,10 @@ export class Render {
|
|
|
543
562
|
return canvas;
|
|
544
563
|
}
|
|
545
564
|
|
|
565
|
+
/**
|
|
566
|
+
*
|
|
567
|
+
* @param {HTMLCanvasElement} canvas
|
|
568
|
+
*/
|
|
546
569
|
destroyCanvas(canvas) {
|
|
547
570
|
if (canvas) {
|
|
548
571
|
this.observer.disconnect();
|
|
@@ -551,12 +574,11 @@ export class Render {
|
|
|
551
574
|
canvas.onmousemove = null;
|
|
552
575
|
canvas.onmouseup = null;
|
|
553
576
|
canvas.onclick = null;
|
|
554
|
-
canvas = null;
|
|
555
577
|
}
|
|
556
578
|
}
|
|
557
579
|
|
|
558
580
|
async screenshot({
|
|
559
|
-
filename = this.sketch.
|
|
581
|
+
filename = this.sketch.key,
|
|
560
582
|
pattern = this.sketch.filenamePattern,
|
|
561
583
|
exportDir = this.sketch.exportDir,
|
|
562
584
|
} = {}) {
|
|
@@ -616,6 +638,8 @@ export class Render {
|
|
|
616
638
|
onStart: (params) => {
|
|
617
639
|
this.time = 0;
|
|
618
640
|
this.elapsed = 0;
|
|
641
|
+
this.playcount = 0;
|
|
642
|
+
this.playheadLast = 0;
|
|
619
643
|
this.thenLoop = performance.now();
|
|
620
644
|
|
|
621
645
|
sketch.beforeRecord.forEach((fn) => fn(params));
|
|
@@ -686,6 +710,10 @@ export class Render {
|
|
|
686
710
|
}
|
|
687
711
|
}
|
|
688
712
|
|
|
713
|
+
/**
|
|
714
|
+
*
|
|
715
|
+
* @param {number} time
|
|
716
|
+
*/
|
|
689
717
|
update(time) {
|
|
690
718
|
if (!this.paused) {
|
|
691
719
|
const deltaTime = time - this.then;
|
|
@@ -705,6 +733,8 @@ export class Render {
|
|
|
705
733
|
invalidate() {
|
|
706
734
|
this.time = 0;
|
|
707
735
|
this.elapsed = 0;
|
|
736
|
+
this.playcount = 0;
|
|
737
|
+
this.playheadLast = 0;
|
|
708
738
|
}
|
|
709
739
|
|
|
710
740
|
dispose() {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import CheckboxInput from './fields/CheckboxInput.svelte';
|
|
5
5
|
import VectorInput from './fields/VectorInput.svelte';
|
|
6
6
|
import TextInput from './fields/TextInput.svelte';
|
|
7
|
+
import TextareaInput from './fields/TextareaInput.svelte';
|
|
7
8
|
import ColorInput from './fields/ColorInput.svelte';
|
|
8
9
|
import ListInput from './fields/ListInput.svelte';
|
|
9
10
|
import ButtonInput from './fields/ButtonInput.svelte';
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
[`${fieldTypes.VEC}`]: VectorInput,
|
|
18
19
|
[`${fieldTypes.CHECKBOX}`]: CheckboxInput,
|
|
19
20
|
[`${fieldTypes.TEXT}`]: TextInput,
|
|
21
|
+
[`${fieldTypes.TEXTAREA}`]: TextareaInput,
|
|
20
22
|
[`${fieldTypes.LIST}`]: ListInput,
|
|
21
23
|
[`${fieldTypes.COLOR}`]: ColorInput,
|
|
22
24
|
[`${fieldTypes.BUTTON}`]: ButtonInput,
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
onchange,
|
|
53
55
|
onclick = () => {},
|
|
54
56
|
children,
|
|
57
|
+
trackChanges = false,
|
|
55
58
|
triggers = $bindable([]),
|
|
56
59
|
} = $props();
|
|
57
60
|
|
|
@@ -63,10 +66,18 @@
|
|
|
63
66
|
|
|
64
67
|
onchange(value);
|
|
65
68
|
},
|
|
69
|
+
/**
|
|
70
|
+
*
|
|
71
|
+
* @param {MouseEvent} event
|
|
72
|
+
*/
|
|
66
73
|
button: (event) => {
|
|
67
74
|
value(event);
|
|
68
75
|
onclick(event);
|
|
69
76
|
},
|
|
77
|
+
/**
|
|
78
|
+
*
|
|
79
|
+
* @param {MouseEvent} event
|
|
80
|
+
*/
|
|
70
81
|
download: async (event) => {
|
|
71
82
|
try {
|
|
72
83
|
let [data, filename] = await value(event);
|
|
@@ -105,7 +116,12 @@
|
|
|
105
116
|
fieldType === fieldTypes.BUTTON),
|
|
106
117
|
);
|
|
107
118
|
let triggersActive = $derived(triggers.length > 0);
|
|
119
|
+
let changed = $derived(trackChanges && !deepEqual(value, initialValue));
|
|
108
120
|
|
|
121
|
+
/**
|
|
122
|
+
*
|
|
123
|
+
* @param {MouseEvent} event
|
|
124
|
+
*/
|
|
109
125
|
function toggleTriggers(event) {
|
|
110
126
|
event.preventDefault();
|
|
111
127
|
|
|
@@ -123,16 +139,15 @@
|
|
|
123
139
|
};
|
|
124
140
|
}
|
|
125
141
|
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
return changed;
|
|
142
|
+
function restoreInitialValue() {
|
|
143
|
+
onchange(initialValue);
|
|
129
144
|
}
|
|
130
145
|
</script>
|
|
131
146
|
|
|
132
147
|
<div
|
|
133
148
|
class="field"
|
|
134
149
|
class:disabled
|
|
135
|
-
class:changed={!disabled &&
|
|
150
|
+
class:changed={!disabled && changed}
|
|
136
151
|
style="--index: {index};"
|
|
137
152
|
>
|
|
138
153
|
<FieldSection
|
|
@@ -166,6 +181,15 @@
|
|
|
166
181
|
<Component {value} {...fieldProps} {onchange} onclick={onTrigger} />
|
|
167
182
|
{@render children?.()}
|
|
168
183
|
</FieldSection>
|
|
184
|
+
{#if changed}
|
|
185
|
+
<button
|
|
186
|
+
class="field__changed"
|
|
187
|
+
onclick={restoreInitialValue}
|
|
188
|
+
title="Restore initial value"
|
|
189
|
+
>
|
|
190
|
+
<span class="visually-hidden">Restore initial value</span>
|
|
191
|
+
</button>
|
|
192
|
+
{/if}
|
|
169
193
|
{#if triggerable}
|
|
170
194
|
<FieldSection {key} visible={showTriggers} secondary>
|
|
171
195
|
<FieldTriggers
|
|
@@ -192,28 +216,45 @@
|
|
|
192
216
|
border-bottom: 1px solid var(--fragment-spacing-color);
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
.
|
|
196
|
-
content: '';
|
|
197
|
-
|
|
219
|
+
.field__changed {
|
|
198
220
|
position: absolute;
|
|
199
221
|
top: 0px;
|
|
200
222
|
left: 0px;
|
|
201
223
|
bottom: 0px;
|
|
202
224
|
z-index: 1;
|
|
203
225
|
|
|
204
|
-
width:
|
|
226
|
+
width: 13px;
|
|
205
227
|
/* height: 4px; */
|
|
206
228
|
/* border-radius: 2px; */
|
|
207
229
|
|
|
208
|
-
|
|
230
|
+
background: transparent;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
|
|
233
|
+
&:before {
|
|
234
|
+
content: '';
|
|
235
|
+
|
|
236
|
+
position: absolute;
|
|
237
|
+
top: 0;
|
|
238
|
+
left: 0;
|
|
239
|
+
|
|
240
|
+
display: block;
|
|
241
|
+
width: 4px;
|
|
242
|
+
height: 100%;
|
|
243
|
+
|
|
244
|
+
--stripes-offset: calc(var(--index) * 1.9px);
|
|
245
|
+
|
|
246
|
+
background: repeating-linear-gradient(
|
|
247
|
+
45deg,
|
|
248
|
+
var(--fragment-accent-color) calc(0px + var(--stripes-offset)),
|
|
249
|
+
var(--fragment-accent-color) calc(2px + var(--stripes-offset)),
|
|
250
|
+
transparent calc(2px + var(--stripes-offset)),
|
|
251
|
+
transparent calc(4px + var(--stripes-offset))
|
|
252
|
+
);
|
|
253
|
+
}
|
|
209
254
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
var(--fragment-accent-color) calc(2px + var(--stripes-offset)),
|
|
214
|
-
transparent calc(2px + var(--stripes-offset)),
|
|
215
|
-
transparent calc(4px + var(--stripes-offset))
|
|
216
|
-
);
|
|
255
|
+
&:hover:before {
|
|
256
|
+
width: 7px;
|
|
257
|
+
}
|
|
217
258
|
}
|
|
218
259
|
|
|
219
260
|
:global(.field__input .field) {
|
|
@@ -229,6 +270,7 @@
|
|
|
229
270
|
.field__actions {
|
|
230
271
|
display: flex;
|
|
231
272
|
align-items: center;
|
|
273
|
+
gap: var(--column-gap);
|
|
232
274
|
}
|
|
233
275
|
|
|
234
276
|
.field__action {
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
}}
|
|
102
102
|
/>
|
|
103
103
|
{/if}
|
|
104
|
-
|
|
104
|
+
{#if rendering.resizing === SIZES.PRESET}
|
|
105
105
|
<Field key="preset">
|
|
106
106
|
<FieldInputRow --grid-template-columns="1fr 1fr">
|
|
107
107
|
<Select
|
|
@@ -123,18 +123,20 @@
|
|
|
123
123
|
/>
|
|
124
124
|
</FieldInputRow>
|
|
125
125
|
</Field>
|
|
126
|
-
{/if}
|
|
127
|
-
|
|
126
|
+
{/if}
|
|
128
127
|
{#if rendering.resizing !== SIZES.PRESET}
|
|
129
128
|
<Field
|
|
130
129
|
key="pixelRatio"
|
|
131
130
|
value={Number(rendering.pixelRatio)}
|
|
132
|
-
onchange={(pixelRatio) =>
|
|
131
|
+
onchange={(pixelRatio) => {
|
|
132
|
+
rendering.pixelRatio = pixelRatio;
|
|
133
|
+
}}
|
|
133
134
|
params={{
|
|
134
135
|
step: 0.1,
|
|
135
136
|
}}
|
|
136
137
|
/>
|
|
137
138
|
{/if}
|
|
139
|
+
|
|
138
140
|
<!-- {#if $sketchesCount > 1 && $monitors.length > 1}
|
|
139
141
|
<ParamsMultisampling />
|
|
140
142
|
{/if} -->
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let {
|
|
3
|
+
label,
|
|
4
|
+
value = $bindable(),
|
|
5
|
+
height,
|
|
6
|
+
disabled = false,
|
|
7
|
+
oninput,
|
|
8
|
+
onchange,
|
|
9
|
+
onkeydown,
|
|
10
|
+
onfocus,
|
|
11
|
+
onblur,
|
|
12
|
+
} = $props();
|
|
13
|
+
|
|
14
|
+
$inspect(height);
|
|
15
|
+
|
|
16
|
+
/** @type {HTMLInputElement} */
|
|
17
|
+
let node;
|
|
18
|
+
|
|
19
|
+
function onKeyPress(event) {
|
|
20
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
21
|
+
node.blur();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
class="input-container"
|
|
28
|
+
class:disabled
|
|
29
|
+
style={height ? `--height: ${height}` : null}
|
|
30
|
+
>
|
|
31
|
+
<textarea
|
|
32
|
+
class="input"
|
|
33
|
+
bind:this={node}
|
|
34
|
+
bind:value
|
|
35
|
+
{oninput}
|
|
36
|
+
{onchange}
|
|
37
|
+
{onkeydown}
|
|
38
|
+
{onfocus}
|
|
39
|
+
{onblur}
|
|
40
|
+
onkeypress={onKeyPress}
|
|
41
|
+
disabled={disabled ? 'disabled' : null}
|
|
42
|
+
autocomplete="off"
|
|
43
|
+
spellcheck="false"
|
|
44
|
+
></textarea>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.input-container {
|
|
49
|
+
position: relative;
|
|
50
|
+
|
|
51
|
+
display: flex;
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: var(--height, var(--fragment-input-height));
|
|
54
|
+
margin: 2px 0;
|
|
55
|
+
|
|
56
|
+
border-radius: var(--fragment-input-border-radius);
|
|
57
|
+
background-color: var(--fragment-input-background-color);
|
|
58
|
+
box-shadow: inset 0 0 0 1px var(--fragment-input-border-color);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:global(body:not(.fragment-dragging))
|
|
62
|
+
.input-container:not(.disabled):hover {
|
|
63
|
+
box-shadow: inset 0 0 0 1px var(--fragment-accent-color);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:global(body:not(.fragment-dragging))
|
|
67
|
+
.input-container:not(.disabled):focus-within {
|
|
68
|
+
box-shadow: 0 0 0 2px var(--fragment-accent-color);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.input {
|
|
72
|
+
width: 100%;
|
|
73
|
+
height: 100%;
|
|
74
|
+
padding: var(--padding) var(--padding);
|
|
75
|
+
|
|
76
|
+
color: var(--fragment-input-text-color);
|
|
77
|
+
font-size: var(--fragment-input-font-size);
|
|
78
|
+
text-align: left;
|
|
79
|
+
|
|
80
|
+
background: transparent;
|
|
81
|
+
outline: 0;
|
|
82
|
+
resize: none;
|
|
83
|
+
border: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.input:disabled {
|
|
87
|
+
color: var(--fragment-input-disabled-text-color);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.input:focus {
|
|
91
|
+
color: var(--fragment-text-color);
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
@@ -6,6 +6,7 @@ export const fieldTypes = {
|
|
|
6
6
|
VEC: 'vec',
|
|
7
7
|
CHECKBOX: 'checkbox',
|
|
8
8
|
TEXT: 'text',
|
|
9
|
+
TEXTAREA: 'textarea',
|
|
9
10
|
LIST: 'list',
|
|
10
11
|
COLOR: 'color',
|
|
11
12
|
BUTTON: 'button',
|
|
@@ -114,7 +115,7 @@ export function inferFieldType({ type, value, params, key }) {
|
|
|
114
115
|
* @param {string} folder
|
|
115
116
|
*/
|
|
116
117
|
export function parseFolder(folder) {
|
|
117
|
-
const regex = /(?<name
|
|
118
|
+
const regex = /(?<name>[\w ]+)(?:\[(?<attributes>[^\]]+)\])?/g;
|
|
118
119
|
const matches = [...folder.matchAll(regex)];
|
|
119
120
|
|
|
120
121
|
const results = matches.map((match) => {
|
|
@@ -124,7 +125,17 @@ export function parseFolder(folder) {
|
|
|
124
125
|
? Object.fromEntries(
|
|
125
126
|
match.groups.attributes
|
|
126
127
|
.split(', ')
|
|
127
|
-
.map((attr) =>
|
|
128
|
+
.map((attr) =>
|
|
129
|
+
attr
|
|
130
|
+
.split('=')
|
|
131
|
+
.map((v) =>
|
|
132
|
+
v === 'false'
|
|
133
|
+
? false
|
|
134
|
+
: v === 'true'
|
|
135
|
+
? true
|
|
136
|
+
: v,
|
|
137
|
+
),
|
|
138
|
+
),
|
|
128
139
|
)
|
|
129
140
|
: {},
|
|
130
141
|
};
|
package/src/index.js
ADDED
package/src/types/index.d.ts
CHANGED
package/src/types/props.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ type BaseProp<Value, Params, Type> = {
|
|
|
2
2
|
value: Value;
|
|
3
3
|
params?: Params;
|
|
4
4
|
type?: Type;
|
|
5
|
+
disabled?: boolean;
|
|
5
6
|
hidden?: boolean;
|
|
6
|
-
displayName?: string;
|
|
7
|
+
displayName?: string | null;
|
|
7
8
|
folder?: string;
|
|
8
9
|
group?: string;
|
|
9
10
|
onChange?: PropOnChange<Value, Params>;
|
|
@@ -25,11 +26,37 @@ type NumberProp = BaseProp<
|
|
|
25
26
|
{ disabled?: boolean; step?: number } | { min: number; max: number },
|
|
26
27
|
'number'
|
|
27
28
|
>;
|
|
28
|
-
|
|
29
|
+
|
|
30
|
+
type VecArray =
|
|
29
31
|
| [number, number]
|
|
30
32
|
| [number, number, number]
|
|
31
|
-
| [number, number, number, number]
|
|
32
|
-
|
|
33
|
+
| [number, number, number, number];
|
|
34
|
+
|
|
35
|
+
type VecObject = Record<string, number> & { [key: number]: never };
|
|
36
|
+
|
|
37
|
+
type VecValue = VecArray | VecObject;
|
|
38
|
+
|
|
39
|
+
type VecArrayParams<V extends VecArray> = {
|
|
40
|
+
min: { [K in keyof V]: number };
|
|
41
|
+
max: { [K in keyof V]: number };
|
|
42
|
+
step?: { [K in keyof V]: number };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type VecObjectParams<V extends VecObject> = {
|
|
46
|
+
min: { [K in keyof V]: number };
|
|
47
|
+
max: { [K in keyof V]: number };
|
|
48
|
+
step?: { [K in keyof V]: number };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type VecParams<V extends VecValue> = V extends readonly number[]
|
|
52
|
+
? VecArrayParams<V>
|
|
53
|
+
: V extends Record<string, number>
|
|
54
|
+
? VecObjectParams<V>
|
|
55
|
+
: never;
|
|
56
|
+
|
|
57
|
+
type VecProp<V extends VecValue = VecValue> = BaseProp<
|
|
58
|
+
V,
|
|
59
|
+
{ locked?: boolean } | VecParams<V>,
|
|
33
60
|
'vec'
|
|
34
61
|
>;
|
|
35
62
|
type CheckboxProp = BaseProp<boolean, never, 'checkbox'>;
|
|
@@ -50,7 +77,8 @@ type ImageProp = BaseProp<string, never, 'image'>;
|
|
|
50
77
|
type Prop =
|
|
51
78
|
| SelectProp
|
|
52
79
|
| NumberProp
|
|
53
|
-
| VecProp
|
|
80
|
+
| VecProp<VecObject>
|
|
81
|
+
| VecProp<VecArray>
|
|
54
82
|
| CheckboxProp
|
|
55
83
|
| TextProp
|
|
56
84
|
| ListProp
|