Files
FreeCADWasmDev/web/src/mesh-primitive-task.tsx
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

87 lines
6.1 KiB
TypeScript

import React, { useEffect, useMemo, useState } from "react";
import type { FacadeResponse } from "./facade-envelope.js";
import { MeshGeometryView, type MeshGeometry } from "./mesh-geometry-view.js";
const fields: Record<string, string[]> = {
box: ["length", "width", "height"], cylinder: ["radius", "length", "edgeLength", "sampling"],
cone: ["radius1", "radius2", "length", "edgeLength", "sampling"], sphere: ["radius", "sampling"],
ellipsoid: ["radius1", "radius2", "sampling"], torus: ["radius1", "radius2", "sampling"],
};
const labels: Record<string, string> = { length: "Length", width: "Width", height: "Height", radius: "Radius",
radius1: "Radius 1", radius2: "Radius 2", edgeLength: "Edge Length", sampling: "Sampling" };
interface Result { action: string; typeId: string; geometry?: MeshGeometry; document?: string; object?: string; file?: { name: string; base64: string } }
export function MeshPrimitiveTask({ response, busy, execute, onCreated }: {
response: FacadeResponse | null; busy: boolean; execute: (args: Record<string, unknown>) => void;
onCreated: (documentId: string, object: string) => void;
}) {
const [solid, setSolid] = useState("box");
const [values, setValues] = useState<Record<string, number>>({ length: 10, width: 10, height: 10, radius: 5, radius1: 10, radius2: 3, edgeLength: 1, sampling: 32 });
const [closed, setClosed] = useState(true);
const [documentName, setDocumentName] = useState("MeshDesign");
const [newDocument, setNewDocument] = useState(true);
const [result, setResult] = useState<Result>();
const [fileStatus, setFileStatus] = useState("");
const [saving, setSaving] = useState(false);
useEffect(() => {
if (response?.kind !== "result") return;
const batch = response.result as { records?: Array<{ apiId: string; status: string; value?: Result }> };
const value = batch.records?.find(row => row.apiId === "Mesh:desktop:Mesh_BuildRegularSolid" && row.status === "exposed")?.value;
if (!value) return;
setResult(value);
if (value.document) {
setDocumentName(value.document); setNewDocument(false);
if (value.object && value.action === "accept") onCreated(value.document, value.object);
}
}, [response]);
const geometries = useMemo(() => result?.geometry ? [result.geometry] : [], [result]);
const submit = (action: string) => {
setResult(undefined);
execute({ action, solid, ...Object.fromEntries(fields[solid].map(key => [key, values[key]])),
closed, document: documentName, documentId: documentName, createDocument: newDocument, selection: [] });
};
const save = async () => {
if (!result?.file) return;
setSaving(true); setFileStatus("");
try {
const root = await navigator.storage.getDirectory();
const directory = await root.getDirectoryHandle("Mesh", { create: true });
const name = `${crypto.randomUUID()}-${result.file.name}`;
const handle = await directory.getFileHandle(name, { create: true });
const writer = await handle.createWritable();
try { await writer.write(Uint8Array.from(atob(result.file.base64), char => char.charCodeAt(0))); await writer.close(); }
catch (error) { await writer.abort().catch(() => undefined); throw error; }
const url = URL.createObjectURL(await handle.getFile());
const link = document.createElement("a"); link.href = url; link.download = result.file.name; link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000); setFileStatus(`Mesh/${name}`);
} catch (error) { setFileStatus(error instanceof Error ? error.message : String(error)); }
finally { setSaving(false); }
};
return <section aria-label="Regular mesh primitive">
<form onSubmit={event => { event.preventDefault(); submit("accept"); }}>
<fieldset disabled={busy}>
<legend>Regular Mesh</legend>
<label>Shape <select value={solid} onChange={event => { setSolid(event.target.value); setResult(undefined); }}>
{Object.keys(fields).map(name => <option key={name} value={name}>{name}</option>)}
</select></label>
{fields[solid].map(key => <label key={key} style={{ display: "block" }}>{labels[key]}
<input type="number" required step={key === "sampling" ? 1 : "any"}
min={key === "sampling" ? 3 : solid === "cone" && key.startsWith("radius") ? 0 : 0.000001}
max={key === "sampling" ? 1000 : undefined} value={Number.isFinite(values[key]) ? values[key] : ""}
onChange={event => setValues(previous => ({ ...previous, [key]: event.target.value === "" ? NaN : Number(event.target.value) }))} />
</label>)}
{["cylinder", "cone"].includes(solid) && <label><input type="checkbox" checked={closed} onChange={event => setClosed(event.target.checked)} />Closed</label>}
<label style={{ display: "block" }}>Document <input required value={documentName} onChange={event => setDocumentName(event.target.value)} /></label>
<label><input type="checkbox" checked={newDocument} onChange={event => setNewDocument(event.target.checked)} />New Document</label>
<button type="button" onClick={event => { if (event.currentTarget.form?.reportValidity()) submit("preview"); }}>Preview</button>
<button type="submit">Create</button>
<button type="button" onClick={() => { setResult(undefined); execute({ action: "cancel" }); }}>Cancel</button>
</fieldset>
</form>
{result?.object && <p>{result.document} / {result.object} ({result.typeId})</p>}
{geometries.length > 0 && <MeshGeometryView geometries={geometries} documentId={result?.document ?? "mesh-preview"} label="Regular mesh primitive" />}
{result?.file && <button disabled={saving} onClick={() => void save()}>Download FCStd</button>}
{fileStatus && <p role="status" style={{ overflowWrap: "anywhere" }}>{fileStatus}</p>}
</section>;
}