react-native-webrtc-kaleidoscope 2.7.4 → 2.7.6
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/README.md +35 -39
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +3 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/index.web.d.ts +2 -2
- package/dist/src/index.web.js +2 -2
- package/dist/src/index.web.js.map +1 -1
- package/dist/src/kaleidoscope/controls.js +1 -1
- package/dist/src/kaleidoscope/controls.js.map +1 -1
- package/dist/src/kaleidoscope/types.d.ts +1 -1
- package/dist/src/kaleidoscope/types.d.ts.map +1 -1
- package/dist/src/kaleidoscope/types.js +3 -2
- package/dist/src/kaleidoscope/types.js.map +1 -1
- package/dist/src/persistence/provider.d.ts.map +1 -1
- package/dist/src/persistence/provider.js +3 -0
- package/dist/src/persistence/provider.js.map +1 -1
- package/package.json +2 -6
- package/src/index.ts +3 -3
- package/src/index.web.ts +2 -2
- package/src/kaleidoscope/controls.ts +1 -1
- package/src/kaleidoscope/types.ts +4 -3
- package/src/persistence/provider.tsx +3 -0
- package/android/src/test/java/com/simiancraft/kaleidoscope/CompositeLayersTest.kt +0 -165
- package/tools/thumbnails/book-loader.ts +0 -104
- package/tools/thumbnails/make-thumbnails.ts +0 -287
- package/tools/thumbnails/office-fixture.webp +0 -0
- package/tools/thumbnails/render-page.ts +0 -259
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/kaleidoscope/types.ts"],"names":[],"mappings":";AAAA,
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/kaleidoscope/types.ts"],"names":[],"mappings":";AAAA,gCAAgC;AAChC,EAAE;AACF,kEAAkE;AAClE,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,8EAA8E;AAC9E,kDAAkD;AAClD,kFAAkF;AAClF,kFAAkF;AAClF,iFAAiF;AACjF,EAAE;AACF,6EAA6E;AAC7E,uEAAuE;AACvE,+EAA+E;AAC/E,8EAA8E","sourcesContent":["// The four-verb surface: types.\n//\n// Bind a track and a preset book once; get four typed verbs back:\n// - kaleidoscope(cmd, patches?) the art axis: which composite (layer stack)\n// fills the frame. cmd is a preset id from the book (narrowed), or null to\n// clear. patches optionally merge per-layer uniform overrides (addressed by\n// layer id); patching the currently-active preset routes through the live\n// no-rebuild channel, so sliders stay smooth.\n// - transform(t?) the geometry axis: absolute flips + 90° rotation.\n// - mask(m) the segmentation edge shared by every art effect.\n// - dispose() tear down the pipeline; release the bound track.\n//\n// Shaders live in the library; consumers add presets (composites) over them,\n// never new shaders. Per shader-world convention, numeric uniforms are\n// normalized 0..1 where practical; ranges are documented in JSDoc as hints for\n// IntelliSense and tooling, not enforced at runtime (validation is userland).\n\nimport type { PatchableShaderName, ShaderUniformsMap } from '../../catalog/shaders';\nimport type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';\n\n/**\n * A live per-layer uniform override for ONE layer, derived from the layer's own\n * type: `id` is the layer's id, `uniforms` is `Partial` of the shader's uniform\n * type (re-indexed from `ShaderUniformsMap` by the layer's literal `shader`).\n * Non-tunable layers (`image`, `direct`) distribute to `never`, so they cannot be\n * patched. The runtime resolves by `id`; the shader is never sent on the wire.\n */\nexport type PatchFor<L> = L extends {\n readonly id: infer I extends string;\n readonly shader: infer S extends PatchableShaderName;\n}\n ? { readonly id: I; readonly uniforms: Partial<ShaderUniformsMap[S]> }\n : never;\n\n/**\n * The patches `kaleidoscope` accepts for preset `K` in book `P`: per-layer\n * overrides, each addressed by one of that preset's tunable layer ids and typed\n * by that layer's shader. At a literal `cmd` call site this narrows to the\n * preset's ids/uniforms; with a variable `cmd` it widens to the book-wide union\n * and is runtime-checked by id.\n */\nexport type PatchesFor<P extends KaleidoscopePresetBook, K extends keyof P> = ReadonlyArray<\n PatchFor<P[K]['layers'][number]>\n>;\n\n/**\n * Absolute, stateless geometric transform. Every call is the full desired state\n * from the identity orientation: re-passing is the caller's responsibility, and\n * `transform()` (or `transform({})`) resets to identity. Rotation snaps to the\n * nearest 90°; arbitrary angles and offset are a later step.\n */\nexport type TransformInput = {\n /** Mirror flips about each axis. */\n readonly flip?: { readonly x?: boolean; readonly y?: boolean };\n /** Clockwise rotation in degrees; snapped to the nearest 90 (0/90/180/270). */\n readonly rotate?: number;\n};\n\n/** The segmentation mask edge, shared by every art effect (not transforms). */\nexport type MaskInput = {\n /** Edge hardness, 0..1. 0 = soft halo, 1 = near-step. */\n readonly hardness: number;\n /** Edge threshold, 0..1. Higher rejects low-confidence (chair-edge) pixels. */\n readonly threshold: number;\n};\n\nexport type KaleidoscopeBindOptions<P extends KaleidoscopePresetBook> = {\n /** The consumer's preset book. Declare it `as const satisfies KaleidoscopePresetBook`. */\n readonly presets: P;\n /**\n * Called with the live output track after every art/transform command. On web\n * each command yields a NEW MediaStreamTrack (the pipeline is rebuilt); on\n * native the same track is mutated in place and passed back.\n */\n readonly onTrack?: (track: MediaStreamTrack) => void;\n};\n\n/**\n * The art verb: select a composite by id (rebuilding the pipeline), or clear it\n * with `null`. When `cmd` is the currently-active preset id and `patches` is\n * given, the patches merge through the live no-rebuild uniform channel (keyed by\n * layer id) instead of rebuilding, so a slider drag stays smooth. On a preset\n * SWITCH, patches merge into the rebuilt layer stack itself, so a restored\n * selection lands tuned on every platform, native included.\n */\ntype KaleidoscopeCommand<P extends KaleidoscopePresetBook> = <K extends keyof P>(\n cmd: K | null,\n patches?: PatchesFor<P, K>,\n) => void;\n\n/**\n * The four verbs for one bound track and book, plus the live track and a\n * teardown. `kaleidoscope` (preset switch) and `transform` rebuild the composite\n * (web yields a new track via onTrack); a `kaleidoscope` patch of the active\n * preset and `mask` both update what the running composite reads each frame, so\n * they need no rebuild.\n */\nexport interface KaleidoscopeBinding<P extends KaleidoscopePresetBook> {\n readonly kaleidoscope: KaleidoscopeCommand<P>;\n readonly transform: (t?: TransformInput) => void;\n readonly mask: (m: MaskInput) => void;\n readonly track: MediaStreamTrack;\n readonly dispose: () => void;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/persistence/provider.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/persistence/provider.tsx"],"names":[],"mappings":"AAuBA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAEhF,OAAO,EAEL,KAAK,sBAAsB,EAI3B,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,sBAAsB,GAAG,sBAAsB,IAAI;IAC9F,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7C,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAClE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IAC5E,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACnF,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAIF,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,sBAAsB,IAAI;IAC7E,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,sBAAsB,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAC9B,CAAC;AAQF,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,sBAAsB,EAAE,EAC1E,OAAO,EACP,KAAK,EACL,WAA0B,EAC1B,QAAQ,GACT,EAAE,8BAA8B,CAAC,CAAC,CAAC,GAAG,YAAY,CA4DlD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,SAAS,sBAAsB,GAAG,sBAAsB,KACtD,sBAAsB,CAAC,CAAC,CAAC,CAY7B"}
|
|
@@ -11,9 +11,12 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
11
11
|
// verbs. The host applies the restored selection itself (gated on `hydrated`,
|
|
12
12
|
// so a stored preset is not flashed over by the default):
|
|
13
13
|
//
|
|
14
|
+
// // `controls` is the binding from bindKaleidoscope(track, { presets }).
|
|
14
15
|
// const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();
|
|
15
16
|
// useEffect(() => {
|
|
16
17
|
// if (!hydrated || !controls) return;
|
|
18
|
+
// // patchesFor reads this render's patches; live edits go through onPatch,
|
|
19
|
+
// // not this re-apply, so it stays out of the deps.
|
|
17
20
|
// if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));
|
|
18
21
|
// else controls.kaleidoscope(null);
|
|
19
22
|
// }, [hydrated, controls, presetId]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../src/persistence/provider.tsx"],"names":[],"mappings":"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","sourcesContent":["// Persistence: the React surface. `KaleidoscopeStateProvider` hydrates the\n// stored selection once at mount and writes through on every change;\n// `useKaleidoscopeState` hands the host the hydrated values plus the setters.\n//\n// The provider owns STORAGE state only; it never binds a track or calls the\n// verbs. The host applies the restored selection itself (gated on `hydrated`,\n// so a stored preset is not flashed over by the default):\n//\n// const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();\n// useEffect(() => {\n// if (!hydrated || !controls) return;\n// if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));\n// else controls.kaleidoscope(null);\n// }, [hydrated, controls, presetId]);\n//\n// The backing store defaults to AsyncStorage; pass any `KaleidoscopeStateStore`\n// to swap it. Importing this subpath is what brings the optional\n// `@react-native-async-storage/async-storage` peer onto your bundle path\n// (Metro resolves it at bundle time either way, so there is no lazy escape).\n\nimport {\n createContext,\n type ReactElement,\n type ReactNode,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport type { MaskInput, PatchesFor } from '../kaleidoscope/types';\nimport type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';\nimport { kaleidoscopeAsyncStorageStore } from './async-storage-store';\nimport {\n DEFAULT_MASK,\n type KaleidoscopeStateStore,\n mergePatch,\n patchListFor,\n pruneStoredState,\n type StoredPatch,\n type StoredPatches,\n} from './state';\n\nexport type KaleidoscopeStateValue<P extends KaleidoscopePresetBook = KaleidoscopePresetBook> = {\n /** False until the persisted selection has been read; apply no effects before then. */\n readonly hydrated: boolean;\n /** The selected preset id, or null when nothing is selected. */\n readonly presetId: (keyof P & string) | null;\n /** The shared segmentation edge. */\n readonly mask: MaskInput;\n /** Every preset's stored per-layer overrides (keyed by preset id, then layer id). */\n readonly patches: StoredPatches;\n readonly setPreset: (presetId: (keyof P & string) | null) => void;\n readonly setMask: (mask: MaskInput) => void;\n /** Record one control-panel patch against a preset (and persist it). */\n readonly setPatch: (presetId: keyof P & string, patch: StoredPatch) => void;\n /** A preset's stored overrides in the array shape `kaleidoscope(id, patches)` takes. */\n readonly patchesFor: <K extends keyof P & string>(presetId: K) => PatchesFor<P, K>;\n /** Clear the stored selection back to defaults (and persist the cleared state). */\n readonly reset: () => void;\n};\n\nconst KaleidoscopeStateContext = createContext<KaleidoscopeStateValue | null>(null);\n\nexport type KaleidoscopeStateProviderProps<P extends KaleidoscopePresetBook> = {\n /** The consumer's preset book; stored state is pruned against it at hydrate. */\n readonly presets: P;\n /** The backing store. Defaults to the AsyncStorage store (lazily loaded). */\n readonly store?: KaleidoscopeStateStore;\n /** The mask used before hydration and after `reset`. Defaults to 0.5/0.5. */\n readonly defaultMask?: MaskInput;\n readonly children: ReactNode;\n};\n\ntype Selection = {\n readonly presetId: string | null;\n readonly mask: MaskInput;\n readonly patches: StoredPatches;\n};\n\nexport function KaleidoscopeStateProvider<P extends KaleidoscopePresetBook>({\n presets,\n store,\n defaultMask = DEFAULT_MASK,\n children,\n}: KaleidoscopeStateProviderProps<P>): ReactElement {\n const [hydrated, setHydrated] = useState(false);\n const [selection, setSelection] = useState<Selection>({\n presetId: null,\n mask: defaultMask,\n patches: {},\n });\n // A write before hydration wins over the stored value (the person acted; do\n // not clobber their fresh choice with yesterday's).\n const dirty = useRef(false);\n // Pinned for the provider's lifetime: hydrate and every write-through go to\n // the same store the first render saw.\n const storeRef = useRef(store ?? kaleidoscopeAsyncStorageStore);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: hydrate exactly once; the book and store are bind-time constants.\n useEffect(() => {\n let cancelled = false;\n storeRef.current.load().then(\n (stored) => {\n if (cancelled) return;\n if (stored && !dirty.current) {\n const pruned = pruneStoredState(stored, presets);\n setSelection({ presetId: pruned.presetId, mask: pruned.mask, patches: pruned.patches });\n }\n setHydrated(true);\n },\n () => {\n if (!cancelled) setHydrated(true);\n },\n );\n return () => {\n cancelled = true;\n };\n }, []);\n\n const commit = (next: Selection): void => {\n dirty.current = true;\n setSelection(next);\n void storeRef.current.save({ version: 1, ...next });\n };\n\n const value: KaleidoscopeStateValue = {\n hydrated,\n presetId: selection.presetId,\n mask: selection.mask,\n patches: selection.patches,\n setPreset: (presetId) => commit({ ...selection, presetId }),\n setMask: (mask) => commit({ ...selection, mask }),\n setPatch: (presetId, patch) =>\n commit({ ...selection, patches: mergePatch(selection.patches, presetId, patch) }),\n // The stored wire shape is book-agnostic; the typed view is recovered at the\n // hook (`useKaleidoscopeState<typeof presets>()`), so the cast is the seam.\n patchesFor: ((presetId: string) =>\n patchListFor(selection.patches, presetId)) as KaleidoscopeStateValue['patchesFor'],\n reset: () => commit({ presetId: null, mask: defaultMask, patches: {} }),\n };\n\n return (\n <KaleidoscopeStateContext.Provider value={value}>{children}</KaleidoscopeStateContext.Provider>\n );\n}\n\n/**\n * The persisted selection plus its setters. Typed by the consumer's book:\n * `useKaleidoscopeState<typeof presets>()`. Throws outside the provider.\n */\nexport function useKaleidoscopeState<\n P extends KaleidoscopePresetBook = KaleidoscopePresetBook,\n>(): KaleidoscopeStateValue<P> {\n const value = useContext(KaleidoscopeStateContext);\n if (value === null) {\n throw new Error(\n 'useKaleidoscopeState: no <KaleidoscopeStateProvider> above this component. ' +\n 'Wrap your app (or the screen using the picker) in the provider from ' +\n \"'react-native-webrtc-kaleidoscope/persistence'.\",\n );\n }\n // Safe by construction: the provider pruned ids against the same book the\n // consumer parameterizes with; the runtime shapes are identical.\n return value as unknown as KaleidoscopeStateValue<P>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../src/persistence/provider.tsx"],"names":[],"mappings":"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","sourcesContent":["// Persistence: the React surface. `KaleidoscopeStateProvider` hydrates the\n// stored selection once at mount and writes through on every change;\n// `useKaleidoscopeState` hands the host the hydrated values plus the setters.\n//\n// The provider owns STORAGE state only; it never binds a track or calls the\n// verbs. The host applies the restored selection itself (gated on `hydrated`,\n// so a stored preset is not flashed over by the default):\n//\n// // `controls` is the binding from bindKaleidoscope(track, { presets }).\n// const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();\n// useEffect(() => {\n// if (!hydrated || !controls) return;\n// // patchesFor reads this render's patches; live edits go through onPatch,\n// // not this re-apply, so it stays out of the deps.\n// if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));\n// else controls.kaleidoscope(null);\n// }, [hydrated, controls, presetId]);\n//\n// The backing store defaults to AsyncStorage; pass any `KaleidoscopeStateStore`\n// to swap it. Importing this subpath is what brings the optional\n// `@react-native-async-storage/async-storage` peer onto your bundle path\n// (Metro resolves it at bundle time either way, so there is no lazy escape).\n\nimport {\n createContext,\n type ReactElement,\n type ReactNode,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport type { MaskInput, PatchesFor } from '../kaleidoscope/types';\nimport type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';\nimport { kaleidoscopeAsyncStorageStore } from './async-storage-store';\nimport {\n DEFAULT_MASK,\n type KaleidoscopeStateStore,\n mergePatch,\n patchListFor,\n pruneStoredState,\n type StoredPatch,\n type StoredPatches,\n} from './state';\n\nexport type KaleidoscopeStateValue<P extends KaleidoscopePresetBook = KaleidoscopePresetBook> = {\n /** False until the persisted selection has been read; apply no effects before then. */\n readonly hydrated: boolean;\n /** The selected preset id, or null when nothing is selected. */\n readonly presetId: (keyof P & string) | null;\n /** The shared segmentation edge. */\n readonly mask: MaskInput;\n /** Every preset's stored per-layer overrides (keyed by preset id, then layer id). */\n readonly patches: StoredPatches;\n readonly setPreset: (presetId: (keyof P & string) | null) => void;\n readonly setMask: (mask: MaskInput) => void;\n /** Record one control-panel patch against a preset (and persist it). */\n readonly setPatch: (presetId: keyof P & string, patch: StoredPatch) => void;\n /** A preset's stored overrides in the array shape `kaleidoscope(id, patches)` takes. */\n readonly patchesFor: <K extends keyof P & string>(presetId: K) => PatchesFor<P, K>;\n /** Clear the stored selection back to defaults (and persist the cleared state). */\n readonly reset: () => void;\n};\n\nconst KaleidoscopeStateContext = createContext<KaleidoscopeStateValue | null>(null);\n\nexport type KaleidoscopeStateProviderProps<P extends KaleidoscopePresetBook> = {\n /** The consumer's preset book; stored state is pruned against it at hydrate. */\n readonly presets: P;\n /** The backing store. Defaults to the AsyncStorage store (lazily loaded). */\n readonly store?: KaleidoscopeStateStore;\n /** The mask used before hydration and after `reset`. Defaults to 0.5/0.5. */\n readonly defaultMask?: MaskInput;\n readonly children: ReactNode;\n};\n\ntype Selection = {\n readonly presetId: string | null;\n readonly mask: MaskInput;\n readonly patches: StoredPatches;\n};\n\nexport function KaleidoscopeStateProvider<P extends KaleidoscopePresetBook>({\n presets,\n store,\n defaultMask = DEFAULT_MASK,\n children,\n}: KaleidoscopeStateProviderProps<P>): ReactElement {\n const [hydrated, setHydrated] = useState(false);\n const [selection, setSelection] = useState<Selection>({\n presetId: null,\n mask: defaultMask,\n patches: {},\n });\n // A write before hydration wins over the stored value (the person acted; do\n // not clobber their fresh choice with yesterday's).\n const dirty = useRef(false);\n // Pinned for the provider's lifetime: hydrate and every write-through go to\n // the same store the first render saw.\n const storeRef = useRef(store ?? kaleidoscopeAsyncStorageStore);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: hydrate exactly once; the book and store are bind-time constants.\n useEffect(() => {\n let cancelled = false;\n storeRef.current.load().then(\n (stored) => {\n if (cancelled) return;\n if (stored && !dirty.current) {\n const pruned = pruneStoredState(stored, presets);\n setSelection({ presetId: pruned.presetId, mask: pruned.mask, patches: pruned.patches });\n }\n setHydrated(true);\n },\n () => {\n if (!cancelled) setHydrated(true);\n },\n );\n return () => {\n cancelled = true;\n };\n }, []);\n\n const commit = (next: Selection): void => {\n dirty.current = true;\n setSelection(next);\n void storeRef.current.save({ version: 1, ...next });\n };\n\n const value: KaleidoscopeStateValue = {\n hydrated,\n presetId: selection.presetId,\n mask: selection.mask,\n patches: selection.patches,\n setPreset: (presetId) => commit({ ...selection, presetId }),\n setMask: (mask) => commit({ ...selection, mask }),\n setPatch: (presetId, patch) =>\n commit({ ...selection, patches: mergePatch(selection.patches, presetId, patch) }),\n // The stored wire shape is book-agnostic; the typed view is recovered at the\n // hook (`useKaleidoscopeState<typeof presets>()`), so the cast is the seam.\n patchesFor: ((presetId: string) =>\n patchListFor(selection.patches, presetId)) as KaleidoscopeStateValue['patchesFor'],\n reset: () => commit({ presetId: null, mask: defaultMask, patches: {} }),\n };\n\n return (\n <KaleidoscopeStateContext.Provider value={value}>{children}</KaleidoscopeStateContext.Provider>\n );\n}\n\n/**\n * The persisted selection plus its setters. Typed by the consumer's book:\n * `useKaleidoscopeState<typeof presets>()`. Throws outside the provider.\n */\nexport function useKaleidoscopeState<\n P extends KaleidoscopePresetBook = KaleidoscopePresetBook,\n>(): KaleidoscopeStateValue<P> {\n const value = useContext(KaleidoscopeStateContext);\n if (value === null) {\n throw new Error(\n 'useKaleidoscopeState: no <KaleidoscopeStateProvider> above this component. ' +\n 'Wrap your app (or the screen using the picker) in the provider from ' +\n \"'react-native-webrtc-kaleidoscope/persistence'.\",\n );\n }\n // Safe by construction: the provider pruned ids against the same book the\n // consumer parameterizes with; the runtime shapes are identical.\n return value as unknown as KaleidoscopeStateValue<P>;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-webrtc-kaleidoscope",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.6",
|
|
4
4
|
"description": "Live video effects (blur, background replacement, generative backgrounds, flip/rotate) for react-native-webrtc, packaged as a managed-Expo-friendly Expo Module. Working on web, Android, and iOS. Active development.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -538,15 +538,11 @@
|
|
|
538
538
|
"./expo-module.config.json": "./expo-module.config.json",
|
|
539
539
|
"./package.json": "./package.json"
|
|
540
540
|
},
|
|
541
|
-
"bin": {
|
|
542
|
-
"kaleidoscope-thumbnails": "./tools/thumbnails/make-thumbnails.ts"
|
|
543
|
-
},
|
|
544
541
|
"files": [
|
|
545
542
|
"src",
|
|
546
543
|
"catalog",
|
|
547
544
|
"dist",
|
|
548
|
-
"
|
|
549
|
-
"android/src",
|
|
545
|
+
"android/src/main",
|
|
550
546
|
"android/build.gradle",
|
|
551
547
|
"ios",
|
|
552
548
|
"app.plugin.js",
|
package/src/index.ts
CHANGED
|
@@ -192,7 +192,7 @@ const specToNativeName = (spec: ReturnType<typeof toEffectSpec>): string => {
|
|
|
192
192
|
const lastAppliedSignatureByTrack = new WeakMap<object, string>();
|
|
193
193
|
|
|
194
194
|
// The lower-level native primitive: route a spec array through the upstream
|
|
195
|
-
// `_setVideoEffects`. Internal now (the public surface is the
|
|
195
|
+
// `_setVideoEffects`. Internal now (the public surface is the four verbs);
|
|
196
196
|
// `bindKaleidoscope`'s reconcile drives it.
|
|
197
197
|
const applyVideoEffects: ApplyVideoEffects = (track, effects) => {
|
|
198
198
|
const t = track as MediaStreamTrack & WebRTCTrackExtensions;
|
|
@@ -264,8 +264,8 @@ const applyVideoEffects: ApplyVideoEffects = (track, effects) => {
|
|
|
264
264
|
};
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
* Bind a track and a preset book; get the
|
|
268
|
-
* (`{ kaleidoscope, transform, mask }`). On native the track is mutated in
|
|
267
|
+
* Bind a track and a preset book; get the four verbs back
|
|
268
|
+
* (`{ kaleidoscope, transform, mask, dispose }`). On native the track is mutated in
|
|
269
269
|
* place, so `controls.track` is the bound track and `onTrack` fires with it
|
|
270
270
|
* after each `kaleidoscope` preset switch and `transform` command. `mask`
|
|
271
271
|
* updates the segmentation edge the per-frame processors read.
|
package/src/index.web.ts
CHANGED
|
@@ -152,8 +152,8 @@ export const applyVideoEffectsDisposable = (
|
|
|
152
152
|
};
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
* Bind a track and a preset book; get the
|
|
156
|
-
* (`{ kaleidoscope, transform, mask }`). Presets live in the consumer's
|
|
155
|
+
* Bind a track and a preset book; get the four verbs back
|
|
156
|
+
* (`{ kaleidoscope, transform, mask, dispose }`). Presets live in the consumer's
|
|
157
157
|
* project; these verbs drive them. On web each `kaleidoscope` preset switch and
|
|
158
158
|
* each `transform` command rebuilds the Insertable-Streams pipeline and yields a
|
|
159
159
|
* new output track, so read the live track from `onTrack` (or `controls.track`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The four-verb controls: shared composite-state machine, platform-agnostic.
|
|
2
2
|
//
|
|
3
3
|
// Holds the art effect (one composite) and the transform op list, reconciles
|
|
4
4
|
// them into an ordered EffectSpec array (art FIRST so segmentation sees the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The four-verb surface: types.
|
|
2
2
|
//
|
|
3
|
-
// Bind a track and a preset book once; get
|
|
3
|
+
// Bind a track and a preset book once; get four typed verbs back:
|
|
4
4
|
// - kaleidoscope(cmd, patches?) the art axis: which composite (layer stack)
|
|
5
5
|
// fills the frame. cmd is a preset id from the book (narrowed), or null to
|
|
6
6
|
// clear. patches optionally merge per-layer uniform overrides (addressed by
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// no-rebuild channel, so sliders stay smooth.
|
|
9
9
|
// - transform(t?) the geometry axis: absolute flips + 90° rotation.
|
|
10
10
|
// - mask(m) the segmentation edge shared by every art effect.
|
|
11
|
+
// - dispose() tear down the pipeline; release the bound track.
|
|
11
12
|
//
|
|
12
13
|
// Shaders live in the library; consumers add presets (composites) over them,
|
|
13
14
|
// never new shaders. Per shader-world convention, numeric uniforms are
|
|
@@ -88,7 +89,7 @@ type KaleidoscopeCommand<P extends KaleidoscopePresetBook> = <K extends keyof P>
|
|
|
88
89
|
) => void;
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* The
|
|
92
|
+
* The four verbs for one bound track and book, plus the live track and a
|
|
92
93
|
* teardown. `kaleidoscope` (preset switch) and `transform` rebuild the composite
|
|
93
94
|
* (web yields a new track via onTrack); a `kaleidoscope` patch of the active
|
|
94
95
|
* preset and `mask` both update what the running composite reads each frame, so
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
// verbs. The host applies the restored selection itself (gated on `hydrated`,
|
|
7
7
|
// so a stored preset is not flashed over by the default):
|
|
8
8
|
//
|
|
9
|
+
// // `controls` is the binding from bindKaleidoscope(track, { presets }).
|
|
9
10
|
// const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();
|
|
10
11
|
// useEffect(() => {
|
|
11
12
|
// if (!hydrated || !controls) return;
|
|
13
|
+
// // patchesFor reads this render's patches; live edits go through onPatch,
|
|
14
|
+
// // not this re-apply, so it stays out of the deps.
|
|
12
15
|
// if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));
|
|
13
16
|
// else controls.kaleidoscope(null);
|
|
14
17
|
// }, [hydrated, controls, presetId]);
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
package com.simiancraft.kaleidoscope
|
|
2
|
-
|
|
3
|
-
import org.junit.Assert.assertArrayEquals
|
|
4
|
-
import org.junit.Assert.assertEquals
|
|
5
|
-
import org.junit.Assert.assertNull
|
|
6
|
-
import org.junit.Assert.assertTrue
|
|
7
|
-
import org.junit.Before
|
|
8
|
-
import org.junit.Test
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Unit tests for the composite layer-stack wire contract: the JSON that
|
|
12
|
-
* serializeCompositeLayers (src/index.ts) sends across the bridge and that
|
|
13
|
-
* CompositeLayers.set/get/clear parses into the snapshot the compositor reads each
|
|
14
|
-
* frame. These pin the parse rules the iOS CompositeLayers.swift port must match;
|
|
15
|
-
* the same cases are mirrored in the iOS XCTest suite, so a divergence in either
|
|
16
|
-
* parser fails on one side.
|
|
17
|
-
*
|
|
18
|
-
* Runs as a JVM unit test (testDebugUnitTest): the real org.json is on the test
|
|
19
|
-
* classpath and android.util.Log is a no-op stub (unitTests.returnDefaultValues).
|
|
20
|
-
*/
|
|
21
|
-
class CompositeLayersTest {
|
|
22
|
-
// CompositeLayers is a process-global singleton; reset between cases so a prior
|
|
23
|
-
// set() can't leak into the next assertion.
|
|
24
|
-
@Before
|
|
25
|
-
fun reset() = CompositeLayers.clear()
|
|
26
|
-
|
|
27
|
-
// The wizard-tower composite exactly as serializeCompositeLayers emits it: a
|
|
28
|
-
// generative base (clouds, uniforms), the cut-out image (image, source = the
|
|
29
|
-
// stable image id), then you (direct, subject). The canonical happy path.
|
|
30
|
-
@Test
|
|
31
|
-
fun parsesWizardTowerStack() {
|
|
32
|
-
CompositeLayers.set(
|
|
33
|
-
"""
|
|
34
|
-
[
|
|
35
|
-
{"id":"sky","shader":"clouds","target":"background","uniforms":{"uExposure":1.26,"uSkyLowColor":[0.99,0.62,0.03],"uCloudSpeed":0.92}},
|
|
36
|
-
{"id":"tower","shader":"image","target":"background","source":"wizards-tower"},
|
|
37
|
-
{"id":"you","shader":"direct","target":"subject"}
|
|
38
|
-
]
|
|
39
|
-
""".trimIndent(),
|
|
40
|
-
)
|
|
41
|
-
val layers = CompositeLayers.get()
|
|
42
|
-
assertEquals(3, layers.size)
|
|
43
|
-
|
|
44
|
-
val clouds = layers[0]
|
|
45
|
-
assertEquals("sky", clouds.id)
|
|
46
|
-
assertEquals("clouds", clouds.shader)
|
|
47
|
-
assertEquals("background", clouds.target)
|
|
48
|
-
assertNull(clouds.blend)
|
|
49
|
-
assertNull(clouds.source)
|
|
50
|
-
assertArrayEquals(floatArrayOf(1.26f), clouds.uniforms["uExposure"], EPS)
|
|
51
|
-
assertArrayEquals(floatArrayOf(0.99f, 0.62f, 0.03f), clouds.uniforms["uSkyLowColor"], EPS)
|
|
52
|
-
|
|
53
|
-
val image = layers[1]
|
|
54
|
-
assertEquals("tower", image.id)
|
|
55
|
-
assertEquals("image", image.shader)
|
|
56
|
-
assertEquals("wizards-tower", image.source)
|
|
57
|
-
assertTrue(image.uniforms.isEmpty())
|
|
58
|
-
|
|
59
|
-
val you = layers[2]
|
|
60
|
-
assertEquals("you", you.id)
|
|
61
|
-
assertEquals("direct", you.shader)
|
|
62
|
-
assertEquals("subject", you.target)
|
|
63
|
-
assertNull(you.source)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// An overlay layer carries a blend mode; additive must round-trip, and the
|
|
67
|
-
// base layer (no blend key) must stay null (the compositor reads null = opaque).
|
|
68
|
-
@Test
|
|
69
|
-
fun parsesAdditiveBlendOverOpaqueBase() {
|
|
70
|
-
CompositeLayers.set(
|
|
71
|
-
"""
|
|
72
|
-
[
|
|
73
|
-
{"id":"image","shader":"image","target":"background","source":"stylized-dark"},
|
|
74
|
-
{"id":"rays","shader":"godrays","target":"background","blend":"additive","uniforms":{"uRayCount":11}}
|
|
75
|
-
]
|
|
76
|
-
""".trimIndent(),
|
|
77
|
-
)
|
|
78
|
-
val layers = CompositeLayers.get()
|
|
79
|
-
assertEquals(2, layers.size)
|
|
80
|
-
assertEquals("image", layers[0].id)
|
|
81
|
-
assertNull(layers[0].blend)
|
|
82
|
-
assertEquals("rays", layers[1].id)
|
|
83
|
-
assertEquals("additive", layers[1].blend)
|
|
84
|
-
assertArrayEquals(floatArrayOf(11f), layers[1].uniforms["uRayCount"], EPS)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// `id` is always on the wire now; a payload missing it falls back to the array
|
|
88
|
-
// index so the address stays stable and unique-per-stack rather than colliding.
|
|
89
|
-
@Test
|
|
90
|
-
fun fallsBackToIndexWhenIdMissing() {
|
|
91
|
-
CompositeLayers.set(
|
|
92
|
-
"""[{"shader":"clouds","uniforms":{}},{"shader":"direct","target":"subject"}]""",
|
|
93
|
-
)
|
|
94
|
-
val layers = CompositeLayers.get()
|
|
95
|
-
assertEquals(2, layers.size)
|
|
96
|
-
assertEquals("0", layers[0].id)
|
|
97
|
-
assertEquals("1", layers[1].id)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// target defaults to "background" when the wire omits it.
|
|
101
|
-
@Test
|
|
102
|
-
fun defaultsMissingTargetToBackground() {
|
|
103
|
-
CompositeLayers.set("""[{"shader":"clouds","uniforms":{}}]""")
|
|
104
|
-
assertEquals("background", CompositeLayers.get()[0].target)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// A scalar uniform normalizes to a one-element array; a numeric array stays a
|
|
108
|
-
// vector, in order.
|
|
109
|
-
@Test
|
|
110
|
-
fun normalizesScalarAndVectorUniforms() {
|
|
111
|
-
CompositeLayers.set("""[{"shader":"plasma","uniforms":{"speed":0.3,"colorA":[0.0,0.3,0.6]}}]""")
|
|
112
|
-
val u = CompositeLayers.get()[0].uniforms
|
|
113
|
-
assertArrayEquals(floatArrayOf(0.3f), u["speed"], EPS)
|
|
114
|
-
assertArrayEquals(floatArrayOf(0.0f, 0.3f, 0.6f), u["colorA"], EPS)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// A uniform whose value is neither a number nor a numeric array is skipped;
|
|
118
|
-
// the shader keeps its GLSL default for that name. Well-formed siblings stay.
|
|
119
|
-
@Test
|
|
120
|
-
fun skipsNonNumericUniformKeepingSiblings() {
|
|
121
|
-
CompositeLayers.set("""[{"shader":"plasma","uniforms":{"speed":0.3,"bad":"nope"}}]""")
|
|
122
|
-
val u = CompositeLayers.get()[0].uniforms
|
|
123
|
-
assertArrayEquals(floatArrayOf(0.3f), u["speed"], EPS)
|
|
124
|
-
assertNull(u["bad"])
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// A layer with no shader is skipped; well-formed siblings survive (the parse
|
|
128
|
-
// is per-layer lenient, not all-or-nothing).
|
|
129
|
-
@Test
|
|
130
|
-
fun skipsLayerWithoutShader() {
|
|
131
|
-
CompositeLayers.set("""[{"target":"background"},{"shader":"direct","target":"subject"}]""")
|
|
132
|
-
val layers = CompositeLayers.get()
|
|
133
|
-
assertEquals(1, layers.size)
|
|
134
|
-
assertEquals("direct", layers[0].shader)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
@Test
|
|
138
|
-
fun parsesEmptyStack() {
|
|
139
|
-
CompositeLayers.set("[]")
|
|
140
|
-
assertTrue(CompositeLayers.get().isEmpty())
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// A whole-payload parse failure (not a JSON array) leaves the previous composite
|
|
144
|
-
// in place rather than blanking the frame.
|
|
145
|
-
@Test
|
|
146
|
-
fun keepsPreviousCompositeOnMalformedPayload() {
|
|
147
|
-
CompositeLayers.set("""[{"shader":"direct","target":"subject"}]""")
|
|
148
|
-
CompositeLayers.set("not json at all")
|
|
149
|
-
val layers = CompositeLayers.get()
|
|
150
|
-
assertEquals(1, layers.size)
|
|
151
|
-
assertEquals("direct", layers[0].shader)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// clear() drops the active composite (a non-composite effect taking over).
|
|
155
|
-
@Test
|
|
156
|
-
fun clearEmptiesStack() {
|
|
157
|
-
CompositeLayers.set("""[{"shader":"direct","target":"subject"}]""")
|
|
158
|
-
CompositeLayers.clear()
|
|
159
|
-
assertTrue(CompositeLayers.get().isEmpty())
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private companion object {
|
|
163
|
-
const val EPS = 1e-6f
|
|
164
|
-
}
|
|
165
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// Loads a preset book as DATA by executing it under Bun with three shims
|
|
2
|
-
// (issue #65). The book is real TypeScript that imports React control
|
|
3
|
-
// components, expo-asset, and bundled images; none of those matter to a
|
|
4
|
-
// thumbnail, so each is replaced with the smallest stand-in that keeps the
|
|
5
|
-
// module graph executable:
|
|
6
|
-
//
|
|
7
|
-
// - `expo-asset`: `Asset.fromModule(m).uri` returns the module value itself,
|
|
8
|
-
// which (via the asset shim below) is the file's absolute path.
|
|
9
|
-
// - control components (`*.form.*`, `*.controls.*`): a Proxy that satisfies
|
|
10
|
-
// any named import without pulling React in; presets reference these as
|
|
11
|
-
// values but thumbnails never render them.
|
|
12
|
-
// - bundled images (`.webp` / `.png` / `.jpg`): the file's absolute path as
|
|
13
|
-
// the default export, so an image layer's `source` resolves to something
|
|
14
|
-
// the CLI can read and embed.
|
|
15
|
-
//
|
|
16
|
-
// Executing (vs static parsing, which the prebuild plugin does for asset
|
|
17
|
-
// collection) is what yields exact per-preset layer stacks and uniform values
|
|
18
|
-
// with no fragile object-literal parsing. This is Bun-only by design; the
|
|
19
|
-
// thumbnail maker is an opt-in dev command, not runtime code.
|
|
20
|
-
|
|
21
|
-
import path from 'node:path';
|
|
22
|
-
import { plugin } from 'bun';
|
|
23
|
-
|
|
24
|
-
/** One layer as authored in a book (the subset thumbnails care about). */
|
|
25
|
-
type LoadedLayer = {
|
|
26
|
-
readonly id?: string;
|
|
27
|
-
readonly shader: string;
|
|
28
|
-
readonly target?: string;
|
|
29
|
-
readonly blend?: string;
|
|
30
|
-
readonly source?: string;
|
|
31
|
-
readonly uniforms?: Record<string, number | readonly number[]>;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** One preset as authored in a book. */
|
|
35
|
-
export type LoadedPreset = {
|
|
36
|
-
readonly name: string;
|
|
37
|
-
readonly taxonomy: readonly string[];
|
|
38
|
-
readonly thumbnail?: string | number;
|
|
39
|
-
readonly layers: readonly LoadedLayer[];
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
plugin({
|
|
43
|
-
name: 'kaleidoscope-thumbnails-book-shims',
|
|
44
|
-
setup(build) {
|
|
45
|
-
// Path-based onLoad (not onResolve, which Bun's runtime plugins do not
|
|
46
|
-
// reliably fire): any file inside the expo-asset package becomes the
|
|
47
|
-
// shim, so its entry never executes and never drags React Native's
|
|
48
|
-
// Flow-typed asset registry in.
|
|
49
|
-
build.onLoad({ filter: /node_modules\/expo-asset\/.*\.[cm]?js$/ }, () => ({
|
|
50
|
-
contents: [
|
|
51
|
-
'function unwrap(m) {',
|
|
52
|
-
' if (typeof m === "string") return m;',
|
|
53
|
-
' try { if (m && typeof m.default === "string") return m.default; } catch {}',
|
|
54
|
-
' try { if (m && typeof m.uri === "string") return m.uri; } catch {}',
|
|
55
|
-
' try { return String(m); } catch { return ""; }',
|
|
56
|
-
'}',
|
|
57
|
-
'export const Asset = { fromModule: (m) => ({ uri: unwrap(m) }) };',
|
|
58
|
-
].join('\n'),
|
|
59
|
-
loader: 'js',
|
|
60
|
-
}));
|
|
61
|
-
// Control components are imported by NAME, and ESM named imports need the
|
|
62
|
-
// name to exist; the name is convention-derived from the filename
|
|
63
|
-
// (`clouds.controls.js` -> CloudsControls, `plasma.form.tsx` -> PlasmaForm),
|
|
64
|
-
// so the stub synthesizes exactly that export.
|
|
65
|
-
build.onLoad({ filter: /\.(form|controls)\.(tsx|jsx|ts|js)$/ }, (args) => {
|
|
66
|
-
const base = path.basename(args.path);
|
|
67
|
-
const stem = base.split('.')[0] ?? 'stub';
|
|
68
|
-
const kind = base.includes('.controls.') ? 'Controls' : 'Form';
|
|
69
|
-
const pascal = stem
|
|
70
|
-
.split('-')
|
|
71
|
-
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
72
|
-
.join('');
|
|
73
|
-
const name = `${pascal}${kind}`;
|
|
74
|
-
return {
|
|
75
|
-
contents: `export const ${name} = () => null;\nexport default ${name};`,
|
|
76
|
-
loader: 'js',
|
|
77
|
-
};
|
|
78
|
-
});
|
|
79
|
-
// Bun materializes plugin modules as ESM namespaces even when authored as
|
|
80
|
-
// CJS, so the path rides the default export; `import x from './x.webp'`
|
|
81
|
-
// unwraps it natively and the Asset shim's unwrap() handles the
|
|
82
|
-
// `require('./x.webp')` namespace form.
|
|
83
|
-
build.onLoad({ filter: /\.(webp|png|jpe?g|gif)$/ }, (args) => ({
|
|
84
|
-
contents: `export default ${JSON.stringify(args.path)};`,
|
|
85
|
-
loader: 'js',
|
|
86
|
-
}));
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Import the book and return its preset map. Accepts the conventional
|
|
92
|
-
* `export const presets` (the demo's shape) or a default export.
|
|
93
|
-
*/
|
|
94
|
-
export async function loadPresetBook(bookPath: string): Promise<Record<string, LoadedPreset>> {
|
|
95
|
-
const abs = path.resolve(bookPath);
|
|
96
|
-
const mod = (await import(abs)) as { presets?: unknown; default?: unknown };
|
|
97
|
-
const book = mod.presets ?? mod.default;
|
|
98
|
-
if (!book || typeof book !== 'object') {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`${bookPath} did not export a preset book (expected \`export const presets = {...}\` or a default export).`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return book as Record<string, LoadedPreset>;
|
|
104
|
-
}
|