feat(web-platform): release V0.8.3 常规界面优化
web-platform-release / Build and publish release (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
web-platform-release / Build and publish release (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
This commit is contained in:
@@ -36,12 +36,20 @@ function BodyBranch({
|
||||
node,
|
||||
depth,
|
||||
onJointHover,
|
||||
onSelectBody,
|
||||
onSelectJoint,
|
||||
selectedBodyId,
|
||||
selectedJointId,
|
||||
searching,
|
||||
query,
|
||||
}: {
|
||||
node: BodyNode;
|
||||
depth: number;
|
||||
onJointHover: (jointId: number | null) => void;
|
||||
onSelectBody?: (bodyId: number) => void;
|
||||
onSelectJoint?: (joint: JointInfo) => void;
|
||||
selectedBodyId?: number;
|
||||
selectedJointId?: number;
|
||||
searching: boolean;
|
||||
query: string;
|
||||
}) {
|
||||
@@ -60,11 +68,15 @@ function BodyBranch({
|
||||
<summary
|
||||
role="treeitem"
|
||||
aria-expanded={shownOpen}
|
||||
aria-selected={selectedBodyId === node.id}
|
||||
tabIndex={0}
|
||||
onClick={(event) => {
|
||||
onSelectBody?.(node.id);
|
||||
if (searching) event.preventDefault();
|
||||
}}
|
||||
className="flex cursor-pointer select-none items-center gap-1.5 truncate rounded px-1.5 py-1 text-xs text-text-secondary hover:bg-element-hover focus-visible:ring-2 focus-visible:ring-accent/30"
|
||||
className={`flex cursor-pointer select-none items-center gap-1.5 truncate rounded px-1.5 py-1 text-xs hover:bg-element-hover focus-visible:ring-2 focus-visible:ring-accent/30 ${
|
||||
selectedBodyId === node.id ? 'bg-accent-soft text-accent' : 'text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
<Box aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||
<span className="truncate">
|
||||
@@ -74,10 +86,14 @@ function BodyBranch({
|
||||
<ul role="group" className="ml-3 border-l border-border pl-1">
|
||||
{node.joints.map((joint) => (
|
||||
<li role="none" key={joint.id}>
|
||||
<span
|
||||
<button
|
||||
type="button"
|
||||
role="treeitem"
|
||||
tabIndex={0}
|
||||
className="flex cursor-default items-center gap-1.5 truncate rounded px-1.5 py-1 text-xs text-warning hover:bg-warning-soft focus:bg-warning-soft focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/30"
|
||||
aria-selected={selectedJointId === joint.id}
|
||||
className={`flex w-full items-center gap-1.5 truncate rounded px-1.5 py-1 text-left text-xs text-warning hover:bg-warning-soft focus:bg-warning-soft focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/30 ${
|
||||
selectedJointId === joint.id ? 'bg-warning-soft' : ''
|
||||
}`}
|
||||
onClick={() => onSelectJoint?.(joint)}
|
||||
onMouseEnter={() => onJointHover(joint.id)}
|
||||
onMouseLeave={() => onJointHover(null)}
|
||||
onFocus={() => onJointHover(joint.id)}
|
||||
@@ -86,7 +102,7 @@ function BodyBranch({
|
||||
>
|
||||
<Disc3 aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
|
||||
<SearchHighlight text={joint.name} query={query} />
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{node.children.map((child) => (
|
||||
@@ -95,6 +111,10 @@ function BodyBranch({
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
onJointHover={onJointHover}
|
||||
onSelectBody={onSelectBody}
|
||||
onSelectJoint={onSelectJoint}
|
||||
selectedBodyId={selectedBodyId}
|
||||
selectedJointId={selectedJointId}
|
||||
searching={searching}
|
||||
query={query}
|
||||
/>
|
||||
@@ -102,14 +122,20 @@ function BodyBranch({
|
||||
</ul>
|
||||
</details>
|
||||
) : (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
role="treeitem"
|
||||
tabIndex={0}
|
||||
className="flex items-center gap-1.5 truncate rounded px-1.5 py-1 text-xs text-text-secondary focus-visible:ring-2 focus-visible:ring-accent/30"
|
||||
aria-selected={selectedBodyId === node.id}
|
||||
className={`flex w-full items-center gap-1.5 truncate rounded px-1.5 py-1 text-left text-xs focus-visible:ring-2 focus-visible:ring-accent/30 ${
|
||||
selectedBodyId === node.id
|
||||
? 'bg-accent-soft text-accent'
|
||||
: 'text-text-secondary hover:bg-element-hover'
|
||||
}`}
|
||||
onClick={() => onSelectBody?.(node.id)}
|
||||
>
|
||||
<Box aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||
<SearchHighlight text={node.name} query={query} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
@@ -169,11 +195,19 @@ export function ModelStructureTree({
|
||||
bodies,
|
||||
joints,
|
||||
onJointHover,
|
||||
onSelectBody,
|
||||
onSelectJoint,
|
||||
selectedBodyId,
|
||||
selectedJointId,
|
||||
query = '',
|
||||
}: {
|
||||
bodies: BodyInfo[];
|
||||
joints: JointInfo[];
|
||||
onJointHover: (jointId: number | null) => void;
|
||||
onSelectBody?: (bodyId: number) => void;
|
||||
onSelectJoint?: (joint: JointInfo) => void;
|
||||
selectedBodyId?: number;
|
||||
selectedJointId?: number;
|
||||
query?: string;
|
||||
}) {
|
||||
const normalized = query.trim().toLocaleLowerCase(),
|
||||
@@ -211,8 +245,12 @@ export function ModelStructureTree({
|
||||
renderRow={(item) =>
|
||||
item.kind === 'body' ? (
|
||||
<div
|
||||
aria-selected={selectedBodyId === item.body.id}
|
||||
onClick={() => onSelectBody?.(item.body.id)}
|
||||
onDoubleClick={() => toggle(item)}
|
||||
className="flex h-full items-center gap-1.5 px-1.5 text-xs text-text-secondary"
|
||||
className={`flex h-full items-center gap-1.5 px-1.5 text-xs ${
|
||||
selectedBodyId === item.body.id ? 'text-accent' : 'text-text-secondary'
|
||||
}`}
|
||||
style={{ paddingLeft: item.depth * 12 + 6 }}
|
||||
>
|
||||
<Box className="h-3.5 w-3.5 text-accent" />
|
||||
@@ -220,8 +258,12 @@ export function ModelStructureTree({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex h-full items-center gap-1.5 px-1.5 text-xs text-warning"
|
||||
aria-selected={selectedJointId === item.joint.id}
|
||||
className={`flex h-full items-center gap-1.5 px-1.5 text-xs text-warning ${
|
||||
selectedJointId === item.joint.id ? 'bg-warning-soft' : ''
|
||||
}`}
|
||||
style={{ paddingLeft: item.depth * 12 + 6 }}
|
||||
onClick={() => onSelectJoint?.(item.joint)}
|
||||
onMouseEnter={() => onJointHover(item.joint.id)}
|
||||
onMouseLeave={() => onJointHover(null)}
|
||||
>
|
||||
@@ -244,6 +286,10 @@ export function ModelStructureTree({
|
||||
node={root}
|
||||
depth={0}
|
||||
onJointHover={onJointHover}
|
||||
onSelectBody={onSelectBody}
|
||||
onSelectJoint={onSelectJoint}
|
||||
selectedBodyId={selectedBodyId}
|
||||
selectedJointId={selectedJointId}
|
||||
searching={Boolean(normalized)}
|
||||
query={query}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { zipSync } from 'fflate';
|
||||
import {
|
||||
choosePreferredEntry,
|
||||
discoverEntries,
|
||||
filesFromDrop,
|
||||
importBrowserFiles,
|
||||
normalizeProjectPath,
|
||||
prepareProjectForMujoco,
|
||||
@@ -35,6 +36,30 @@ describe('project importer', () => {
|
||||
expect(entries).toHaveLength(3);
|
||||
expect(choosePreferredEntry(entries)).toBe('model.xml');
|
||||
});
|
||||
it('通过现代文件系统句柄递归读取拖入的文件夹', async () => {
|
||||
const model = new File(['<mujoco/>'], 'model.xml', { type: 'text/xml' });
|
||||
const fileHandle = {
|
||||
kind: 'file',
|
||||
name: 'model.xml',
|
||||
getFile: async () => model,
|
||||
};
|
||||
const directoryHandle = {
|
||||
kind: 'directory',
|
||||
name: 'robot',
|
||||
async *values() {
|
||||
yield fileHandle;
|
||||
},
|
||||
};
|
||||
const items = [
|
||||
{
|
||||
kind: 'file',
|
||||
getAsFileSystemHandle: async () => directoryHandle,
|
||||
},
|
||||
] as unknown as DataTransferItemList;
|
||||
const files = await filesFromDrop(items, [] as unknown as FileList);
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].webkitRelativePath).toBe('robot/model.xml');
|
||||
});
|
||||
it('异步解压 ZIP、保留二进制数据并报告阶段进度', async () => {
|
||||
const zipped = zipSync({
|
||||
'robot/model.urdf': encode('<robot name="r"/>'),
|
||||
|
||||
@@ -427,6 +427,29 @@ export async function importBrowserFiles(
|
||||
return manifest(files[0].path.split('/')[0] || '工程', files);
|
||||
}
|
||||
|
||||
interface DroppedFileSystemHandle {
|
||||
kind: 'file' | 'directory';
|
||||
name: string;
|
||||
getFile?: () => Promise<File>;
|
||||
values?: () => AsyncIterableIterator<DroppedFileSystemHandle>;
|
||||
}
|
||||
|
||||
async function readFileSystemHandle(handle: DroppedFileSystemHandle, prefix = ''): Promise<File[]> {
|
||||
if (handle.kind === 'file' && handle.getFile) {
|
||||
const file = await handle.getFile();
|
||||
Object.defineProperty(file, 'webkitRelativePath', {
|
||||
configurable: true,
|
||||
value: `${prefix}${file.name}`,
|
||||
});
|
||||
return [file];
|
||||
}
|
||||
if (handle.kind !== 'directory' || !handle.values) return [];
|
||||
const files: File[] = [];
|
||||
for await (const child of handle.values())
|
||||
files.push(...(await readFileSystemHandle(child, `${prefix}${handle.name}/`)));
|
||||
return files;
|
||||
}
|
||||
|
||||
interface LegacyEntry {
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
@@ -464,7 +487,27 @@ export async function filesFromDrop(
|
||||
items: DataTransferItemList,
|
||||
fallback: FileList,
|
||||
): Promise<File[]> {
|
||||
const entries = Array.from(items)
|
||||
const fileItems = Array.from(items).filter((item) => item.kind === 'file');
|
||||
const handleRequests = fileItems
|
||||
.map((item) => {
|
||||
const getter = (
|
||||
item as unknown as {
|
||||
getAsFileSystemHandle?: () => Promise<DroppedFileSystemHandle | null>;
|
||||
}
|
||||
).getAsFileSystemHandle;
|
||||
return getter?.call(item);
|
||||
})
|
||||
.filter((request): request is Promise<DroppedFileSystemHandle | null> => Boolean(request));
|
||||
if (handleRequests.length > 0 && handleRequests.length === fileItems.length) {
|
||||
const settled = await Promise.allSettled(handleRequests);
|
||||
const handles = settled.flatMap((result) =>
|
||||
result.status === 'fulfilled' && result.value ? [result.value] : [],
|
||||
);
|
||||
if (handles.length === fileItems.length)
|
||||
return (await Promise.all(handles.map((handle) => readFileSystemHandle(handle)))).flat();
|
||||
}
|
||||
|
||||
const entries = fileItems
|
||||
.map(
|
||||
(item) =>
|
||||
(item as unknown as { webkitGetAsEntry?: () => LegacyEntry | null }).webkitGetAsEntry?.() ??
|
||||
|
||||
Reference in New Issue
Block a user