mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-12 04:42:16 +00:00
frontend: Sonar reliability fixes (promises in conditions, async executors, identical branches, reduce, regex) (#2821)
Clears 51 open TypeScript issues. Typecheck and build are left to CI.
**typescript:S6544 (17)**
- A cached promise checked by truthiness (`if (existing)`, `if
(cached)`, `if (ammoLoadPromise)`…) is checked as `!== undefined` / `!==
null` against its map or field type. Same branches, since a Promise is
always truthy.
- `scene.ts` `load_glb_wrapped_no_cors` / `load_splat_wrapped_no_cors`:
the fetch moves out of the `async` Promise executor. The methods were
already `async`, so a failed fetch now `throw`s (rejects the same way),
and only the callback APIs (`GLTFLoader.parse`, `SplatMesh` `onLoad`)
stay inside `new Promise`.
- `SceneManagerAPI` declares the promises its implementation returns
(`create`/`retrieve` → `Promise<THREE.Object3D | undefined>`,
`update`/`delete` → `Promise<void>`). Nothing calls through the
interface.
- `convertFbxToGlb.worker.ts`: `onmessage` calls `void convert(event)`.
**typescript:S3923 / S1764 / S3516 / S905 (12)**: identical branches
collapsed (`variant === "world" ? "max-w-[1600px]" : "max-w-[1600px]"`,
`ScissorIcon : ScissorIcon`, `index % 2 === 0 ? point + offset : point +
offset`, PromptBox3D's two `setIsEnqueueing(false); return;` arms,
`{DEFAULT_GRID_CONFIG ? null : null}` and its now-unused import), `x &&
x`, the `this.gridHelper;` statement, and `CurrentUserBadge`, which
always rendered `null` (its markup was commented out).
**typescript:S6959 (5)**: each `reduce` starts from the first element.
Every one is behind an emptiness check, and comparing the first element
with itself keeps it.
**typescript:S2871 (4)**: `sort((a, b) => a.localeCompare(b))` for the
order-independent cache keys and the board and font name lists.
**typescript:S8786 (8)**
- Markdown link text is `[^\][]+`. `@image(url, size)` drops the `\s*`
before the size (it's `.trim()`ed already). ATX headings `trimStart()`
the text instead of `\s*`. Slugs trim one `-` (runs were already
collapsed to one).
- `color.ts` `stripCssNoise`: `!important` is replaced by a space, then
trailing `;` are trimmed in a loop.
- Data-URL mime: `/:([^;]*);/`.
- `ControlPanelSceneObject` number check: `/^-?\d*(\.\d*)?$/`. The old
`.` was unescaped and matched any character, so `12a3` passed as a
number.
**Others**: `window.open(…, "noopener,noreferrer")` (S5148).
`AlertTitle` renders `children` explicitly (S6850). The folder name hash
wraps with `Math.imul(…, 1)` instead of `| 0`, the same int32 result
(S7767). `autoFocus` is removed from the two rename inputs, which their
refs already focus and select (S9379).
**Not here**: S7059 in `editor.ts` (`ensureAmmoLoaded()` in the
constructor) is on a line #2807 changes.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved reliability when loading 3D scene assets and completing
drag-and-drop operations.
* Corrected numeric input validation, scene import handling, and video
duration tie handling.
* Improved media, audio, and video cache handling to avoid unnecessary
retries or missed waits.
* Refined markdown parsing for links, headings, image directives, and
heading anchors.
* Discord invites now open with additional browser protections.
* Improved CSS color cleanup and handling of pasted scene elements.
* **Improvements**
* Made ordering of tags, fonts, and prompt-related items more
consistent.
* Improved video editor timeline selections.
* Removed the non-functional account badge from the video editor header.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
**typescript:S1848 (2)**: `new SplatMesh({ …, onLoad })` created an
instance only for its callback. `scene.ts` now awaits `mesh.initialized`
(Spark's promise, which resolves after load), so a failed load rejects
instead of leaving the promise pending. `viewer-3d.tsx` runs the same
body in `initialized.then`, with a `.catch` that logs.
Kept as is: `TabState.setActiveTab` still resolves `true` (S3516 stays
open), because `TabState.spec.ts` and the tab-change benchmark read its
result.
This commit is contained in:
1 parent
71adfd4701
commit
f9f4c99e4f
34 files changed
+97
-129
No files matched your search
@@ -944,7 +944,7 @@ export const ImageTo3DExperience = ({
|
||||
<div
|
||||
className={twMerge(
|
||||
"mx-auto grid h-full w-full grid-cols-[1fr_300px] gap-4 overflow-hidden pb-10",
|
||||
variant === "world" ? "max-w-[1600px]" : "max-w-[1600px]",
|
||||
"max-w-[1600px]",
|
||||
)}
|
||||
style={{ height: `calc(100vh - ${bottomOffsetPx + 80}px)` }}
|
||||
>
|
||||
@@ -1111,7 +1111,7 @@ export const ImageTo3DExperience = ({
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center justify-between gap-2.5",
|
||||
hasResults ? "mt-3" : "mt-3",
|
||||
"mt-3",
|
||||
)}
|
||||
>
|
||||
<ClassyModelSelector
|
||||
|
||||
@@ -135,7 +135,7 @@ export const ButtonDropdown = ({
|
||||
)}
|
||||
<div className="grow text-start">{option.label}</div>
|
||||
<div className="ml-6 font-mono text-[11px] font-normal text-white/45">
|
||||
{option.description && option.description}
|
||||
{option.description}
|
||||
</div>
|
||||
{showSelected && (
|
||||
<>
|
||||
|
||||
@@ -64,7 +64,6 @@ export const FolderNameDialog: React.FC<FolderNameDialogProps> = ({
|
||||
if (e.key === "Enter") submit();
|
||||
}}
|
||||
className="w-full rounded-[3px] border border-ui-panel-border bg-ui-controls/40 px-3 py-1.5 text-sm text-base-fg outline-none focus:ring-1 focus:ring-primary/50"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
|
||||
@@ -403,7 +403,7 @@ const SIDEBAR_TAG_LIMIT = 5;
|
||||
|
||||
const hashString = (s: string): number => {
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
|
||||
for (let i = 0; i < s.length; i++) h = Math.imul(Math.imul(h, 31) + s.charCodeAt(i), 1); // int32 wrap
|
||||
return h;
|
||||
};
|
||||
|
||||
@@ -2710,7 +2710,6 @@ export const GalleryModal = React.memo(
|
||||
}}
|
||||
onBlur={handleConfirmRename}
|
||||
className="text-lg font-semibold bg-transparent px-1.5 py-0 text-base-fg outline-none min-w-[6rem] caret-primary"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<h2
|
||||
|
||||
@@ -87,7 +87,7 @@ export function useMediaPromptTokens(
|
||||
if (t) cleaned.push(t);
|
||||
}
|
||||
// Stable cache key for the effect's dep list, order-independent.
|
||||
const key = [...new Set(cleaned)].sort().join(",");
|
||||
const key = [...new Set(cleaned)].sort((a, b) => a.localeCompare(b)).join(",");
|
||||
|
||||
useEffect(() => {
|
||||
if (key.length > 0) fetchMissing(key.split(","));
|
||||
|
||||
@@ -60,7 +60,7 @@ export function usePrompts(
|
||||
}
|
||||
// Stable cache key for the effect's dep list. Sorting keeps it stable when
|
||||
// a poll returns the same set in a different order.
|
||||
const key = [...new Set(cleaned)].sort().join(",");
|
||||
const key = [...new Set(cleaned)].sort((a, b) => a.localeCompare(b)).join(",");
|
||||
|
||||
useEffect(() => {
|
||||
if (key.length > 0) fetchMissing(key.split(","));
|
||||
|
||||
@@ -104,7 +104,7 @@ export function LoginModal({
|
||||
};
|
||||
|
||||
const handleDiscordJoin = () => {
|
||||
window.open("https://discord.gg/75svZP2Vje", "_blank");
|
||||
window.open("https://discord.gg/75svZP2Vje", "_blank", "noopener,noreferrer");
|
||||
setShowDiscord(false);
|
||||
setShowSuccess(true);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export const collectTags = (board: Board): string[] => {
|
||||
board.itemOrder.forEach((id) => {
|
||||
board.items[id]?.tags.forEach((t) => seen.add(t));
|
||||
});
|
||||
return Array.from(seen).sort();
|
||||
return Array.from(seen).sort((a, b) => a.localeCompare(b));
|
||||
};
|
||||
|
||||
const itemMatchesText = (it: BoardItem, q: string): boolean => {
|
||||
|
||||
@@ -1011,9 +1011,7 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
const pastedLineNode: LineNode = {
|
||||
...lineNode,
|
||||
id: newId,
|
||||
points: lineNode.points.map((point, index) =>
|
||||
index % 2 === 0 ? point + offset : point + offset,
|
||||
),
|
||||
points: lineNode.points.map((point) => point + offset),
|
||||
x: lineNode.x !== undefined ? lineNode.x + offset : undefined,
|
||||
y: lineNode.y !== undefined ? lineNode.y + offset : undefined,
|
||||
};
|
||||
@@ -1131,7 +1129,7 @@ export const useSceneStore = create<SceneState>((set, get, store) => ({
|
||||
|
||||
const base64ToFile = (base64: string, filename: string): File => {
|
||||
const arr = base64.split(",");
|
||||
const mime = arr[0].match(/:(.*?);/)?.[1] || "image/png";
|
||||
const mime = arr[0].match(/:([^;]*);/)?.[1] || "image/png";
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
|
||||
@@ -273,7 +273,7 @@ class DndAsset {
|
||||
return;
|
||||
}
|
||||
const inspection = this.clipNodeNamesCache.get(item.media_id);
|
||||
if (!inspection) {
|
||||
if (inspection === undefined) {
|
||||
// No inspection in flight (shouldn't happen — pointer-down starts
|
||||
// one): add unvalidated, matching the ungated click-to-add path.
|
||||
this.completeAnimationDrop(editor, item, target, undefined);
|
||||
|
||||
+1
-1
@@ -245,7 +245,7 @@ export const ControlPanelSceneObject = () => {
|
||||
if (value === "" || value === "-" || value === ".") {
|
||||
return true;
|
||||
}
|
||||
return !/^-?[0-9]*(.[0-9]*)?$/.test(value);
|
||||
return !/^-?\d*(\.\d*)?$/.test(value);
|
||||
});
|
||||
|
||||
const handlePositionChange = (xyz: Record<string, string>) => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
let ammoLoadPromise: Promise<void> | null = null;
|
||||
|
||||
export const ensureAmmoLoaded = (): Promise<void> => {
|
||||
if (ammoLoadPromise) return ammoLoadPromise;
|
||||
if (ammoLoadPromise !== null) return ammoLoadPromise;
|
||||
|
||||
ammoLoadPromise = new Promise<void>((resolve, reject) => {
|
||||
if (typeof document === "undefined") {
|
||||
|
||||
@@ -91,7 +91,6 @@ class Scene {
|
||||
) {
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.gridHelper;
|
||||
this.scene = new THREE.Scene();
|
||||
this.hot_items = [];
|
||||
|
||||
@@ -1026,21 +1025,20 @@ class Scene {
|
||||
onComplete: () => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<GLTF> {
|
||||
return new Promise<GLTF>(async (resolve, reject) => {
|
||||
let buffer;
|
||||
try {
|
||||
// Host-supplied CORS-bypassed binary fetch (FetchProxy under
|
||||
// Tauri; plain fetch under web hosts that allow it).
|
||||
buffer = await (
|
||||
await this.deps.fetchAsset(media_url, { signal })
|
||||
).arrayBuffer();
|
||||
} catch (error) {
|
||||
if ((error as { name?: string })?.name !== "AbortError") {
|
||||
console.error("load GLB from Tauri error:", error);
|
||||
}
|
||||
reject(error);
|
||||
return;
|
||||
let buffer: ArrayBuffer;
|
||||
try {
|
||||
// Host-supplied CORS-bypassed binary fetch (FetchProxy under
|
||||
// Tauri; plain fetch under web hosts that allow it).
|
||||
buffer = await (
|
||||
await this.deps.fetchAsset(media_url, { signal })
|
||||
).arrayBuffer();
|
||||
} catch (error) {
|
||||
if ((error as { name?: string })?.name !== "AbortError") {
|
||||
console.error("load GLB from Tauri error:", error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return new Promise<GLTF>((resolve, reject) => {
|
||||
const glbLoader = new GLTFLoader();
|
||||
glbLoader.parse(
|
||||
buffer,
|
||||
@@ -1068,30 +1066,23 @@ class Scene {
|
||||
onComplete: () => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SplatMesh> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let buffer;
|
||||
try {
|
||||
// Host-supplied CORS-bypassed binary fetch (FetchProxy under
|
||||
// Tauri; plain fetch under web hosts that allow it).
|
||||
buffer = await (
|
||||
await this.deps.fetchAsset(media_url, { signal })
|
||||
).arrayBuffer();
|
||||
} catch (error) {
|
||||
if ((error as { name?: string })?.name !== "AbortError") {
|
||||
console.error("load Splat from Tauri error:", error);
|
||||
}
|
||||
reject(error);
|
||||
return;
|
||||
let buffer: ArrayBuffer;
|
||||
try {
|
||||
// Host-supplied CORS-bypassed binary fetch (FetchProxy under
|
||||
// Tauri; plain fetch under web hosts that allow it).
|
||||
buffer = await (
|
||||
await this.deps.fetchAsset(media_url, { signal })
|
||||
).arrayBuffer();
|
||||
} catch (error) {
|
||||
if ((error as { name?: string })?.name !== "AbortError") {
|
||||
console.error("load Splat from Tauri error:", error);
|
||||
}
|
||||
|
||||
new SplatMesh({
|
||||
fileBytes: buffer,
|
||||
onLoad: (mesh) => {
|
||||
resolve(mesh);
|
||||
onComplete();
|
||||
},
|
||||
});
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const mesh = new SplatMesh({ fileBytes: buffer });
|
||||
await mesh.initialized;
|
||||
onComplete();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,15 +32,19 @@ export type SceneObject = {
|
||||
};
|
||||
|
||||
export interface SceneManagerAPI {
|
||||
create(media_token: string, name: string, position: THREE.Vector3): void;
|
||||
retrieve(object_uuid: string): void;
|
||||
delete(object_uuid: string): void;
|
||||
create(
|
||||
media_token: string,
|
||||
name: string,
|
||||
position: THREE.Vector3,
|
||||
): Promise<THREE.Object3D | undefined>;
|
||||
retrieve(object_uuid: string): Promise<THREE.Object3D | undefined>;
|
||||
delete(object_uuid: string): Promise<void>;
|
||||
update(
|
||||
object_uuid: string,
|
||||
position: THREE.Vector3,
|
||||
rotation: THREE.Euler,
|
||||
scale: THREE.Vector3,
|
||||
): void;
|
||||
): Promise<void>;
|
||||
selected(): void;
|
||||
render_outliner(timeline_characters: { [key: string]: ClipGroup }): void;
|
||||
select_object(id: string): void;
|
||||
|
||||
@@ -22,7 +22,6 @@ import { CameraSettingsModal } from "@storyteller/ui-camera-settings-modal";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import {
|
||||
CommandSuccessStatus,
|
||||
GenerateImage,
|
||||
GenerateImageRequest,
|
||||
} from "@storyteller/tauri-api";
|
||||
@@ -509,13 +508,8 @@ export const PromptBox3D = ({
|
||||
|
||||
console.log("generateResponse", generateResponse);
|
||||
|
||||
if (generateResponse.status === CommandSuccessStatus.Success) {
|
||||
setIsEnqueueing(false);
|
||||
return;
|
||||
} else {
|
||||
setIsEnqueueing(false);
|
||||
return;
|
||||
}
|
||||
setIsEnqueueing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
+5
-1
@@ -23,7 +23,7 @@ const scope = self as unknown as {
|
||||
postMessage: (message: ConvertResponse, transfer?: Transferable[]) => void;
|
||||
};
|
||||
|
||||
scope.onmessage = async (event) => {
|
||||
const convert = async (event: MessageEvent<ConvertRequest>) => {
|
||||
const { id, buffer } = event.data;
|
||||
try {
|
||||
const group = new FBXLoader().parse(buffer, "");
|
||||
@@ -46,3 +46,7 @@ scope.onmessage = async (event) => {
|
||||
scope.postMessage({ id, ok: false, error: String(error) });
|
||||
}
|
||||
};
|
||||
|
||||
scope.onmessage = (event) => {
|
||||
void convert(event);
|
||||
};
|
||||
@@ -100,8 +100,10 @@ export class PasteCommand extends Command {
|
||||
placementResult.trackIndex - updatedTracks.overlay.length - 1
|
||||
];
|
||||
if (targetTrack?.id === updatedTracks.main.id) {
|
||||
const earliestElement = elementsToAdd.reduce((earliest, element) =>
|
||||
element.startTime < earliest.startTime ? element : earliest,
|
||||
const earliestElement = elementsToAdd.reduce(
|
||||
(earliest, element) =>
|
||||
element.startTime < earliest.startTime ? element : earliest,
|
||||
elementsToAdd[0],
|
||||
);
|
||||
const adjustedEarliestStartTime = enforceMainTrackStart({
|
||||
tracks: updatedTracks,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { RenameProjectDialog } from "../../project/components/rename-project-dia
|
||||
import { DeleteProjectDialog } from "../../project/components/delete-project-dialog";
|
||||
import { cn } from "../../utils/ui";
|
||||
import { Button } from "../ui/button";
|
||||
import { useAuthUser } from "./use-auth-user";
|
||||
|
||||
export interface EditorHeaderProps {
|
||||
// Optional host chrome rendered to the right of the export button (e.g.
|
||||
@@ -39,7 +38,6 @@ export function EditorHeader({ endSlot, exitTo = "/projects" }: EditorHeaderProp
|
||||
<EditableProjectName />
|
||||
</div>
|
||||
<nav className="flex items-center gap-2">
|
||||
<CurrentUserBadge />
|
||||
<ExportButton />
|
||||
{endSlot}
|
||||
</nav>
|
||||
@@ -47,21 +45,6 @@ export function EditorHeader({ endSlot, exitTo = "/projects" }: EditorHeaderProp
|
||||
);
|
||||
}
|
||||
|
||||
function CurrentUserBadge() {
|
||||
const { authUser } = useEditorAdapters();
|
||||
const user = useAuthUser(authUser);
|
||||
if (!user) return null;
|
||||
return (
|
||||
// <span
|
||||
// className="text-muted-foreground hidden truncate text-xs sm:inline-block"
|
||||
// title={user.displayName}
|
||||
// >
|
||||
// {user.displayName}
|
||||
// </span>
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectDropdown({ exitTo }: { exitTo: string }) {
|
||||
const [openDialog, setOpenDialog] = useState<
|
||||
"delete" | "rename" | "shortcuts" | null
|
||||
|
||||
@@ -44,7 +44,7 @@ Alert.displayName = "Alert";
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn(
|
||||
@@ -52,7 +52,9 @@ const AlertTitle = React.forwardRef<
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</h5>
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
|
||||
@@ -539,7 +539,7 @@ export class AudioManager {
|
||||
}): Promise<AudioBuffer | null> {
|
||||
const cacheKey = this.buildPreparedClipCacheKey({ clip });
|
||||
const existing = this.preparedClipBuffers.get(cacheKey);
|
||||
if (existing) {
|
||||
if (existing !== undefined) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -574,7 +574,7 @@ export class AudioManager {
|
||||
clip: AudioClipSource;
|
||||
}): Promise<AudioBuffer | null> {
|
||||
const existing = this.decodedBuffers.get(clip.sourceKey);
|
||||
if (existing) {
|
||||
if (existing !== undefined) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export function clearFontAtlasCache(): void {
|
||||
|
||||
export function loadFontAtlas(): Promise<FontAtlas | null> {
|
||||
if (cachedAtlas) return Promise.resolve(cachedAtlas);
|
||||
if (atlasFetchPromise) return atlasFetchPromise;
|
||||
if (atlasFetchPromise !== null) return atlasFetchPromise;
|
||||
|
||||
atlasFetchPromise = fetch(FONT_ATLAS_PATH)
|
||||
.then(async (response) => {
|
||||
|
||||
@@ -46,7 +46,9 @@ export function useFontAtlas({ open }: { open: boolean }) {
|
||||
|
||||
const fontNames = useMemo(() => {
|
||||
if (!atlas) return [];
|
||||
return [...Object.keys(atlas.fonts), ...SYSTEM_FONTS].sort();
|
||||
return [...Object.keys(atlas.fonts), ...SYSTEM_FONTS].sort((a, b) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
}, [atlas]);
|
||||
|
||||
return { atlas, status, fontNames, retry };
|
||||
|
||||
@@ -7,7 +7,6 @@ import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
GRID_MIN,
|
||||
GRID_MAX,
|
||||
DEFAULT_GRID_CONFIG,
|
||||
} from "../grid";
|
||||
import { usePreviewStore } from "../../preview/preview-store";
|
||||
import { clampRound } from "../../utils/math";
|
||||
@@ -113,9 +112,6 @@ function GridGuideOptions() {
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{/* Acknowledge DEFAULT_GRID_CONFIG so the reset import stays parked
|
||||
for when NumberField (with its onReset affordance) is ported. */}
|
||||
{DEFAULT_GRID_CONFIG ? null : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ function pickClosestScaleCandidate<T extends { distance: number; edge: ScaleEdge
|
||||
return shouldPreferCandidate && !shouldPreferBestCandidate
|
||||
? candidate
|
||||
: bestCandidate;
|
||||
});
|
||||
}, candidates[0]);
|
||||
}
|
||||
|
||||
export function snapPosition({
|
||||
@@ -124,8 +124,10 @@ export function snapPosition({
|
||||
if (snapCandidatesWithinThreshold.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return snapCandidatesWithinThreshold.reduce((closest, current) =>
|
||||
current.distance < closest.distance ? current : closest,
|
||||
return snapCandidatesWithinThreshold.reduce(
|
||||
(closest, current) =>
|
||||
current.distance < closest.distance ? current : closest,
|
||||
snapCandidatesWithinThreshold[0],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function loadImageSource({
|
||||
const cacheKey = `${url}::${maxSourceSize ?? "full"}`;
|
||||
|
||||
const cached = imageSourceCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const promise = (async (): Promise<CachedImageSource> => {
|
||||
const image = new Image();
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export function loadStickerSource({
|
||||
stickerId: string;
|
||||
}): Promise<CachedStickerSource> {
|
||||
const cached = stickerSourceCache.get(stickerId);
|
||||
if (cached) return cached;
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const promise = (async (): Promise<CachedStickerSource> => {
|
||||
const url = resolveStickerId({
|
||||
|
||||
@@ -170,7 +170,7 @@ export class VideoCache {
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (sinkData.prefetching && sinkData.prefetchPromise) {
|
||||
if (sinkData.prefetching && sinkData.prefetchPromise !== null) {
|
||||
await sinkData.prefetchPromise;
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ export class VideoCache {
|
||||
time: number;
|
||||
}): Promise<WrappedCanvas | null> {
|
||||
try {
|
||||
if (sinkData.prefetching && sinkData.prefetchPromise) {
|
||||
if (sinkData.prefetching && sinkData.prefetchPromise !== null) {
|
||||
await sinkData.prefetchPromise;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export class WaveformCache {
|
||||
audioUrl,
|
||||
}: GetSourceWaveformSummaryArgs): Promise<SourceWaveformSummary> {
|
||||
const existing = this.summaries.get(sourceKey);
|
||||
if (existing) {
|
||||
if (existing !== undefined) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
+1
-8
@@ -47,7 +47,6 @@ import { mediaSupportsAudio } from "../../media/media-utils";
|
||||
import {
|
||||
canToggleSourceAudio,
|
||||
getSourceAudioActionLabel,
|
||||
isSourceAudioSeparated,
|
||||
} from "../audio-separation";
|
||||
import { buildWaveformGainSamples, isElementMuted } from "../audio-state";
|
||||
import { getTimelinePixelsPerSecond } from "../pixel-utils";
|
||||
@@ -346,8 +345,6 @@ export function TimelineElement({
|
||||
element.type === "video"
|
||||
? getSourceAudioActionLabel({ element })
|
||||
: "Extract audio";
|
||||
const isElementSourceAudioSeparated =
|
||||
element.type === "video" && isSourceAudioSeparated({ element });
|
||||
const hasKeyframes = elementKeyframes.length > 0;
|
||||
const expansionHeight = getExpansionHeight({ rows: expandedRows });
|
||||
const baseTrackHeight = getTrackHeight({ type: track.type });
|
||||
@@ -450,11 +447,7 @@ export function TimelineElement({
|
||||
{canToggleCurrentSourceAudio && (
|
||||
<ContextMenuItem
|
||||
icon={
|
||||
<HugeiconsIcon
|
||||
icon={
|
||||
isElementSourceAudioSeparated ? ScissorIcon : ScissorIcon
|
||||
}
|
||||
/>
|
||||
<HugeiconsIcon icon={ScissorIcon} />
|
||||
}
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -26,7 +26,7 @@ export function getEarliestMainTrackElement({
|
||||
return element.startTime < earliestElement.startTime
|
||||
? element
|
||||
: earliestElement;
|
||||
});
|
||||
}, elements[0]);
|
||||
}
|
||||
|
||||
export function enforceMainTrackStart({
|
||||
|
||||
@@ -54,10 +54,8 @@ export function appendAlpha({
|
||||
|
||||
function stripCssNoise({ text }: { text: string }): string {
|
||||
let cleaned = text.trim();
|
||||
cleaned = cleaned
|
||||
.replace(/\s*!important\s*/gi, "")
|
||||
.replace(/;+\s*$/, "")
|
||||
.trim();
|
||||
cleaned = cleaned.replace(/!important/gi, " ").trim();
|
||||
while (cleaned.endsWith(";")) cleaned = cleaned.slice(0, -1).trimEnd();
|
||||
|
||||
const colonIndex = cleaned.indexOf(":");
|
||||
const parenIndex = cleaned.indexOf("(");
|
||||
|
||||
@@ -408,9 +408,8 @@ export function Viewer3D({
|
||||
|
||||
if (SPLAT_EXTENSIONS.includes(urlExtension)) {
|
||||
console.log(`[Viewer3D] splat format detected (.${urlExtension})`);
|
||||
new SplatMesh({
|
||||
url: modelUrl,
|
||||
onLoad: (mesh) => {
|
||||
void new SplatMesh({ url: modelUrl }).initialized
|
||||
.then((mesh) => {
|
||||
if (cancelled) {
|
||||
mesh.dispose();
|
||||
return;
|
||||
@@ -434,8 +433,10 @@ export function Viewer3D({
|
||||
}
|
||||
|
||||
console.log("Splat loaded");
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("[Viewer3D] splat load failed:", error),
|
||||
);
|
||||
} else if (!GLTF_EXTENSIONS.includes(urlExtension)) {
|
||||
// Known non-glTF model format with no loader wired here (obj, fbx,
|
||||
// pmx, …). Show the fallback shape rather than spamming a parse error.
|
||||
|
||||
@@ -143,7 +143,7 @@ export const markdownToHtml = (
|
||||
return renderImage(safeUrl, alt, size);
|
||||
});
|
||||
// links: [text](url)
|
||||
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => {
|
||||
out = out.replace(/\[([^\][]+)\]\(([^)]+)\)/g, (_m, label, url) => {
|
||||
const safeLabel = String(label || "").replace(/"/g, """);
|
||||
const safeUrl = normalizeLink(String(url || ""));
|
||||
return `<a href="${safeUrl}" class="underline hover:opacity-90">${safeLabel}</a>`;
|
||||
@@ -220,7 +220,7 @@ export const markdownToHtml = (
|
||||
|
||||
// Image/GIF embed: @image(url, size) or @gif(url, size)
|
||||
const imageMatch = ltrim.match(
|
||||
/^@(image|gif)\(([^,)]+)(?:,\s*([^)]+))?\)\s*$/,
|
||||
/^@(image|gif)\(([^,)]+)(?:,([^)]+))?\)\s*$/,
|
||||
);
|
||||
if (imageMatch) {
|
||||
flushParagraph();
|
||||
@@ -232,12 +232,12 @@ export const markdownToHtml = (
|
||||
}
|
||||
|
||||
// ATX headings: support #..###### with optional space after hashes
|
||||
const heading = ltrim.match(/^(#{1,6})\s*(.*)$/);
|
||||
const heading = ltrim.match(/^(#{1,6})(.*)$/);
|
||||
if (heading) {
|
||||
flushParagraph();
|
||||
closeListIfOpen();
|
||||
const level = Math.min(heading[1].length, 6);
|
||||
const text = renderInline(heading[2] || "");
|
||||
const text = renderInline((heading[2] || "").trimStart());
|
||||
htmlParts.push(`<h${level}>${text}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export const slugify = (s: string): string =>
|
||||
.replace(/[_\s]+/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
.replace(/--+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
.replace(/^-|-$/g, "");
|
||||
|
||||
export const pathToFilename = (path: string): string => {
|
||||
const parts = path.split("/");
|
||||
|
||||
@@ -70,7 +70,7 @@ export function resolveVideoDuration(
|
||||
(distance === bestDistance && option > best)
|
||||
? option
|
||||
: best;
|
||||
});
|
||||
}, model.durationOptions[0]);
|
||||
}
|
||||
return model.defaultDuration ?? null;
|
||||
}
|
||||
Reference in new issue
Block a user