fix(claw): assemble legacy 3d models by default
This commit is contained in:
parent
2817aab197
commit
9717196264
@ -14,6 +14,7 @@ import type {
|
||||
} from "@/lib/openclaw-chat/model-interaction";
|
||||
import {
|
||||
getOpenClawExplodedOffsetMm,
|
||||
getOpenClawSourceNodeOffset,
|
||||
mapOpenClawCadOffsetToGlb,
|
||||
} from "@/lib/openclaw-chat/model-interaction";
|
||||
|
||||
@ -154,6 +155,7 @@ function ModelInteractionScene({
|
||||
resetAllToken,
|
||||
resetSelectedToken,
|
||||
selectedNodeName,
|
||||
sourceVariant,
|
||||
transformRequest,
|
||||
}: {
|
||||
allowDragging: boolean;
|
||||
@ -164,12 +166,31 @@ function ModelInteractionScene({
|
||||
resetAllToken: number;
|
||||
resetSelectedToken: number;
|
||||
selectedNodeName: string | null;
|
||||
sourceVariant: "assembled" | "exploded";
|
||||
transformRequest: OpenClawModelTransformRequest;
|
||||
}) {
|
||||
const { camera, gl, scene: rootScene } = useThree();
|
||||
const controlsRef = useRef<OrbitControlsImpl | null>(null);
|
||||
const gltf = useGLTF(modelUrl, true);
|
||||
const scene = useMemo(() => cloneScene(gltf.scene), [gltf.scene]);
|
||||
const scene = useMemo(() => {
|
||||
const cloned = cloneScene(gltf.scene);
|
||||
if (sourceVariant !== "exploded") return cloned;
|
||||
const partsByNodeName = new Map(
|
||||
manifest.parts.map((part) => [part.nodeName, part])
|
||||
);
|
||||
cloned.traverse((node) => {
|
||||
const part = partsByNodeName.get(node.name);
|
||||
if (!part) return;
|
||||
const offset = getOpenClawSourceNodeOffset(
|
||||
part,
|
||||
sourceVariant,
|
||||
manifest.scaleMmToGlb
|
||||
);
|
||||
node.position.add(new THREE.Vector3(offset.x, offset.y, offset.z));
|
||||
});
|
||||
cloned.updateMatrixWorld(true);
|
||||
return cloned;
|
||||
}, [gltf.scene, manifest, sourceVariant]);
|
||||
const fitTransform = useMemo(() => getFitTransform(scene), [scene]);
|
||||
const entriesRef = useRef<Map<string, InteractiveEntry>>(new Map());
|
||||
const objectEntriesRef = useRef<Map<Object3D, InteractiveEntry>>(new Map());
|
||||
@ -543,6 +564,7 @@ export function OpenClawInteractiveModelViewer({
|
||||
resetSelectedToken,
|
||||
resetViewToken,
|
||||
selectedNodeName,
|
||||
sourceVariant,
|
||||
transformRequest,
|
||||
}: {
|
||||
allowDragging: boolean;
|
||||
@ -556,6 +578,7 @@ export function OpenClawInteractiveModelViewer({
|
||||
resetSelectedToken: number;
|
||||
resetViewToken: number;
|
||||
selectedNodeName: string | null;
|
||||
sourceVariant: "assembled" | "exploded";
|
||||
transformRequest: OpenClawModelTransformRequest;
|
||||
}) {
|
||||
return (
|
||||
@ -595,6 +618,7 @@ export function OpenClawInteractiveModelViewer({
|
||||
resetAllToken={resetAllToken}
|
||||
resetSelectedToken={resetSelectedToken}
|
||||
selectedNodeName={selectedNodeName}
|
||||
sourceVariant={sourceVariant}
|
||||
transformRequest={transformRequest}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
||||
import {
|
||||
buildOpenClawArtifactProxyUrl,
|
||||
getOpenClaw3DArtifactVariant,
|
||||
getOpenClaw3DArtifactBundle,
|
||||
type OpenClawArtifact,
|
||||
} from "@/lib/openclaw-chat/artifacts";
|
||||
@ -203,7 +204,14 @@ export function OpenClawModel3DPreview({
|
||||
const isAssembledModel =
|
||||
model.artifactRole.toLowerCase() === "assembled_3d" ||
|
||||
model.viewer?.variant === "assembled";
|
||||
const supportsNodeInteraction = Boolean(manifest && isAssembledModel);
|
||||
const sourceVariant = getOpenClaw3DArtifactVariant(model);
|
||||
const supportsNodeInteraction = Boolean(
|
||||
manifest && (isAssembledModel || sourceVariant === "exploded")
|
||||
);
|
||||
const displayedModelName =
|
||||
supportsNodeInteraction && sourceVariant === "exploded"
|
||||
? "可旋转3D整装成品模型"
|
||||
: model.displayName || model.filename || "3D 模型";
|
||||
const canDragParts = Boolean(
|
||||
supportsNodeInteraction &&
|
||||
(model.viewer?.partDragging || manifest?.parts.some((part) => part.draggable))
|
||||
@ -259,7 +267,7 @@ export function OpenClawModel3DPreview({
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Box className="h-4 w-4 shrink-0 text-[#687386] dark:text-zinc-400" />
|
||||
<span className="truncate text-[13px] font-medium text-[#2f3640] dark:text-zinc-200">
|
||||
{model.displayName || model.filename || "3D 模型"}
|
||||
{displayedModelName}
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] uppercase text-[#9aa2af] dark:text-zinc-500">
|
||||
{model.viewer?.format || "glb"}
|
||||
@ -321,6 +329,7 @@ export function OpenClawModel3DPreview({
|
||||
resetSelectedToken={resetSelectedToken}
|
||||
resetViewToken={resetViewToken}
|
||||
selectedNodeName={selectedPart?.nodeName || null}
|
||||
sourceVariant={sourceVariant}
|
||||
transformRequest={transformRequest}
|
||||
/>
|
||||
|
||||
|
||||
@ -274,6 +274,22 @@ export type OpenClaw3DArtifactBundle = {
|
||||
previewIndex: number;
|
||||
};
|
||||
|
||||
export function getOpenClaw3DArtifactVariant(
|
||||
artifact: OpenClawArtifact
|
||||
): "assembled" | "exploded" {
|
||||
if (artifact.viewer?.variant) return artifact.viewer.variant;
|
||||
const role = artifact.artifactRole.toLowerCase();
|
||||
const filename = artifact.filename.toLowerCase();
|
||||
if (
|
||||
role === "interactive_3d" ||
|
||||
filename.includes("exploded") ||
|
||||
filename.includes("interactive")
|
||||
) {
|
||||
return "exploded";
|
||||
}
|
||||
return "assembled";
|
||||
}
|
||||
|
||||
export function getOpenClaw3DArtifactBundle(
|
||||
artifacts: OpenClawArtifact[]
|
||||
): OpenClaw3DArtifactBundle | null {
|
||||
|
||||
@ -287,3 +287,15 @@ export function mapOpenClawCadOffsetToGlb(
|
||||
z: -offset.y * scaleMmToGlb,
|
||||
};
|
||||
}
|
||||
|
||||
export function getOpenClawSourceNodeOffset(
|
||||
part: OpenClawModelInteractionPart,
|
||||
sourceVariant: "assembled" | "exploded",
|
||||
scaleMmToGlb: number
|
||||
): OpenClawModelVector {
|
||||
if (sourceVariant === "assembled") return { x: 0, y: 0, z: 0 };
|
||||
const explodedOffset = getOpenClawExplodedOffsetMm(part);
|
||||
if (!explodedOffset) return { x: 0, y: 0, z: 0 };
|
||||
const mapped = mapOpenClawCadOffsetToGlb(explodedOffset, scaleMmToGlb);
|
||||
return { x: -mapped.x, y: -mapped.y, z: -mapped.z };
|
||||
}
|
||||
|
||||
@ -2,12 +2,14 @@ import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
buildOpenClawArtifactProxyUrl,
|
||||
extractOpenClawArtifacts,
|
||||
getOpenClaw3DArtifactVariant,
|
||||
getOpenClaw3DArtifactBundle,
|
||||
isOpenClawInteractive3DArtifact,
|
||||
normalizeOpenClawArtifacts,
|
||||
} from "@/lib/openclaw-chat/artifacts";
|
||||
import {
|
||||
getOpenClawExplodedOffsetMm,
|
||||
getOpenClawSourceNodeOffset,
|
||||
mapOpenClawCadOffsetToGlb,
|
||||
normalizeOpenClawModelInteractionManifest,
|
||||
} from "@/lib/openclaw-chat/model-interaction";
|
||||
@ -132,6 +134,34 @@ test.describe("OpenClaw 3D artifacts", () => {
|
||||
expect(bundle?.previewIndex).toBe(3);
|
||||
});
|
||||
|
||||
test("treats legacy interactive-only GLB as exploded source and can restore assembly", () => {
|
||||
const artifacts = normalizeOpenClawArtifacts([modelFile, manifestFile], {
|
||||
baseUrl: "https://claw.example.com",
|
||||
});
|
||||
const bundle = getOpenClaw3DArtifactBundle(artifacts);
|
||||
expect(bundle?.model.artifactRole).toBe("interactive_3d");
|
||||
expect(getOpenClaw3DArtifactVariant(bundle!.model)).toBe("exploded");
|
||||
|
||||
const manifest = normalizeOpenClawModelInteractionManifest({
|
||||
schema: "dalongxia.furniture-model-interaction.v2",
|
||||
coordinates: { scale_mm_to_glb: 0.001 },
|
||||
parts: [
|
||||
{
|
||||
node_name: "DOOR-001:1",
|
||||
part_id: "DOOR-001",
|
||||
exploded_offset_mm: { x: 2726, y: 120, z: -40 },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
getOpenClawSourceNodeOffset(
|
||||
manifest!.parts[0],
|
||||
"exploded",
|
||||
manifest!.scaleMmToGlb
|
||||
)
|
||||
).toEqual({ x: -2.726, y: 0.04, z: 0.12 });
|
||||
});
|
||||
|
||||
test("normalizes v2 interaction transforms and CAD-to-GLB coordinates", () => {
|
||||
const manifest = normalizeOpenClawModelInteractionManifest({
|
||||
schema: "dalongxia.furniture-model-interaction.v2",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user