87 lines
6.1 KiB
TypeScript
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>;
|
|
}
|