Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions studio/app/api/embeds/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,49 @@ const postSchema = zfd.formData({
projectId: zfd.numeric(),
name: zfd.text(),
onlyTooltipInfo: zfd.checkbox().optional(),
savedViewIds: z.array(z.coerce.number()).optional(),
});

export async function POST(req: Request) {
try {
const data = postSchema.parse(await req.formData());
const formData = await req.formData();
console.log("Received form data keys:", Array.from(formData.keys()));
console.log("savedViewIds values:", formData.getAll("savedViewIds"));
console.log("onlyTooltipInfo value:", formData.get("onlyTooltipInfo"));

const data = postSchema.parse(formData);
console.log("Parsed data:", {
projectId: data.projectId,
name: data.name,
onlyTooltipInfo: data.onlyTooltipInfo,
savedViewIds: data.savedViewIds,
});

const model = await createEmbed(
data.projectId,
data.name,
data.dataFile,
data.thumbnailFileContents,
data.onlyTooltipInfo ?? false,
Array.isArray(data.savedViewIds) ? data.savedViewIds : [],
);

return Response.json(model, { status: 201 });
} catch (e) {
if (e instanceof z.ZodError) {
return new Response(e.message, { status: 400 });
console.error("Validation error:", e.errors);
return new Response(JSON.stringify({ error: "Validation failed", details: e.errors }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
throw e;
console.error("Other error:", e);
return new Response(
JSON.stringify({ error: "Internal server error", message: e instanceof Error ? e.message : "Unknown error" }),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
}
29 changes: 27 additions & 2 deletions studio/features/api-sdk/uploadEmbed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default async function uploadEmbed(
thumbnailFileContents: string,
name: string,
onlyTooltipInfo?: boolean,
savedViewIds?: number[],
) {
const formData = new FormData();

Expand All @@ -15,7 +16,31 @@ export default async function uploadEmbed(
formData.append("name", name);
if (onlyTooltipInfo) formData.append("onlyTooltipInfo", "on");

const response = await axios.post("/api/embeds", formData);
// Add saved view IDs to form data
if (savedViewIds && savedViewIds.length > 0) {
savedViewIds.forEach((id) => {
formData.append("savedViewIds", id.toString());
});
}

return response;
console.log("Uploading embed with data:", {
projectId,
name,
onlyTooltipInfo,
savedViewIds,
formDataKeys: Array.from(formData.keys()),
});

try {
const response = await axios.post("/api/embeds", formData);
console.log("Upload successful:", response.data);
return response;
} catch (error) {
console.error("Upload failed:", error);
if (axios.isAxiosError(error) && error.response) {
console.error("Response data:", error.response.data);
console.error("Response status:", error.response.status);
}
throw error;
}
}
17 changes: 17 additions & 0 deletions studio/features/db/entities/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import {
Column,
CreateDateColumn,
Entity,
JoinTable,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
import { Project } from "./project";
import { SavedView } from "./savedView";

@Entity("embeds")
export class Embed {
Expand All @@ -23,6 +26,20 @@ export class Embed {

@Column({ default: false }) onlyTooltipInfo!: boolean;

@ManyToMany(() => SavedView)
@JoinTable({
name: "embeds_saved_views_saved_views",
joinColumn: {
name: "embedsId",
referencedColumnName: "id",
},
inverseJoinColumn: {
name: "savedViewsId",
referencedColumnName: "id",
},
})
savedViews?: SavedView[];

@CreateDateColumn() createdAt!: Date;
@UpdateDateColumn() updatedAt!: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddEmbedSavedViewsRelationship1750715800001 implements MigrationInterface {
name = "AddEmbedSavedViewsRelationship1750715800001";

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "embeds_saved_views_saved_views" ("embedsId" integer NOT NULL, "savedViewsId" integer NOT NULL, CONSTRAINT "PK_embeds_saved_views_relationship" PRIMARY KEY ("embedsId", "savedViewsId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_embeds_saved_views_embeds" ON "embeds_saved_views_saved_views" ("embedsId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_embeds_saved_views_saved_views" ON "embeds_saved_views_saved_views" ("savedViewsId")`,
);
await queryRunner.query(
`ALTER TABLE "embeds_saved_views_saved_views" ADD CONSTRAINT "FK_embeds_saved_views_embeds" FOREIGN KEY ("embedsId") REFERENCES "embeds"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "embeds_saved_views_saved_views" ADD CONSTRAINT "FK_embeds_saved_views_saved_views" FOREIGN KEY ("savedViewsId") REFERENCES "saved_views"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "embeds_saved_views_saved_views" DROP CONSTRAINT "FK_embeds_saved_views_saved_views"`,
);
await queryRunner.query(
`ALTER TABLE "embeds_saved_views_saved_views" DROP CONSTRAINT "FK_embeds_saved_views_embeds"`,
);
await queryRunner.query(`DROP INDEX "IDX_embeds_saved_views_saved_views"`);
await queryRunner.query(`DROP INDEX "IDX_embeds_saved_views_embeds"`);
await queryRunner.query(`DROP TABLE "embeds_saved_views_saved_views"`);
}
}
97 changes: 69 additions & 28 deletions studio/features/editor-exports/components/EditorExportsCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,40 @@ import uploadEmbed from "@features/api-sdk/uploadEmbed";
import useMetadataContext from "@features/editor-metadata/hooks/useMetadataContext";
import useExportEmbed from "@features/editor/hooks/useExportModels";
import { useRenderer } from "@features/editor/hooks/useRender";
import { useCallback, useState } from "react";
import { useSavedViews } from "@features/saved-views/hooks/useSavedViews";
import { useCallback, useEffect, useState } from "react";

type EditorExportsCreateProps = {
sanitizedId: number;
};

export default function EditorExportsCreate({
sanitizedId,
}: EditorExportsCreateProps) {
export default function EditorExportsCreate({ sanitizedId }: EditorExportsCreateProps) {
const { columns } = useMetadataContext();
const { savedViews, fetchSavedViews } = useSavedViews(sanitizedId);

const [name, setName] = useState<string>("Untitled Embed");
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [selectedSavedViewKeys, setSelectedSavedViewKeys] = useState<string[]>([]);
const [isSavingDialogOpen, setIsSavingDialogOpen] = useState(false);
const [onlyTooltipInfo, setOnlyTooltipInfo] = useState(false);

const exportEmbeds = useExportEmbed();
const renderer = useRenderer();

useEffect(() => {
fetchSavedViews();
}, [fetchSavedViews]);

const saveEmbed = useCallback(() => {
async function handleUploadEmbed(dataFile: File, thumbnailFileContents: string) {
await uploadEmbed(sanitizedId, dataFile, thumbnailFileContents, name, onlyTooltipInfo);
await uploadEmbed(
sanitizedId,
dataFile,
thumbnailFileContents,
name,
onlyTooltipInfo,
selectedSavedViewKeys.map((id) => parseInt(id)),
);

setIsSavingDialogOpen(false);
}
Expand All @@ -62,7 +74,7 @@ export default function EditorExportsCreate({
//upload project version
void handleUploadEmbed(dataFile, image);
};
}, [sanitizedId, name, exportEmbeds, renderer, selectedKeys, onlyTooltipInfo]);
}, [sanitizedId, name, exportEmbeds, renderer, selectedKeys, selectedSavedViewKeys, onlyTooltipInfo]);

const handleGlobalAction = useCallback(
(key: Key) => {
Expand All @@ -77,27 +89,62 @@ export default function EditorExportsCreate({
<PositioningContainer>
<Flex direction="column" height="100%" marginX="size-200">
<View width="100%" marginTop="size-200" marginBottom="size-100">
<TextField
label="Export Name"
width="100%"
value={name}
onChange={setName}
/>
<TextField label="Export Name" width="100%" value={name} onChange={setName} />
<Checkbox isSelected={onlyTooltipInfo} onChange={setOnlyTooltipInfo} marginTop="size-200">
Only include tooltip information
Only include tooltip information
</Checkbox>
</View>

{/* Saved Views Selection */}
<View width="100%" marginBottom="size-50">
<Text
UNSAFE_style={{
fontSize: "var(--spectrum-fieldlabel-text-size, var(--spectrum-global-dimension-font-size-75))",
fontWeight: "var(--spectrum-fieldlabel-text-font-weight, var(--spectrum-global-font-weight-regular))",
lineHeight: "var(--spectrum-fieldlabel-text-line-height, var(--spectrum-global-font-line-height-small))",
color: "var(--spectrum-fieldlabel-text-color, var(--spectrum-alias-label-text-color))",
WebkitFontSmoothing: "subpixel-antialiased",
MozOsxFontSmoothing: "auto",
fontSmooth: "subpixel-antialiased",
}}
>
Choose saved views to include (optional)
</Text>
</View>
<View position="relative" height="size-1000" overflow="hidden" marginBottom="size-200">
<ActionBarContainer height="100%" width="100%">
<ListView
aria-label="Saved views list"
width="100%"
height="100%"
items={savedViews}
selectionMode="multiple"
selectedKeys={selectedSavedViewKeys}
onSelectionChange={(keys) => {
if (keys === "all") {
setSelectedSavedViewKeys(savedViews.map((item) => item.id.toString()));
} else {
setSelectedSavedViewKeys(Array.from(keys) as string[]);
}
}}
>
{(item) => (
<Item key={item.id.toString()} textValue={item.name}>
<Text>{item.name}</Text>
</Item>
)}
</ListView>
</ActionBarContainer>
</View>

{/* Columns Selection */}
<View width="100%" marginBottom="size-50">
<Text
UNSAFE_style={{
fontSize:
"var(--spectrum-fieldlabel-text-size, var(--spectrum-global-dimension-font-size-75))",
fontWeight:
"var(--spectrum-fieldlabel-text-font-weight, var(--spectrum-global-font-weight-regular))",
lineHeight:
"var(--spectrum-fieldlabel-text-line-height, var(--spectrum-global-font-line-height-small))",
color:
"var(--spectrum-fieldlabel-text-color, var(--spectrum-alias-label-text-color))",
fontSize: "var(--spectrum-fieldlabel-text-size, var(--spectrum-global-dimension-font-size-75))",
fontWeight: "var(--spectrum-fieldlabel-text-font-weight, var(--spectrum-global-font-weight-regular))",
lineHeight: "var(--spectrum-fieldlabel-text-line-height, var(--spectrum-global-font-line-height-small))",
color: "var(--spectrum-fieldlabel-text-color, var(--spectrum-alias-label-text-color))",
WebkitFontSmoothing: "subpixel-antialiased",
MozOsxFontSmoothing: "auto",
fontSmooth: "subpixel-antialiased",
Expand All @@ -106,13 +153,7 @@ export default function EditorExportsCreate({
Choose columns to export
</Text>
</View>
<View
position="relative"
flex
height="100%"
overflow="hidden"
marginBottom="size-200"
>
<View position="relative" flex height="100%" overflow="hidden" marginBottom="size-200">
<ActionBarContainer height="100%" width="100%">
<ListView
aria-label="Column list"
Expand Down
82 changes: 82 additions & 0 deletions studio/features/editor-toolbar/components/SavedViewsToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Item, Picker, Tooltip, TooltipTrigger, View } from "@adobe/react-spectrum";
import { ProjectionType } from "@features/bananagl/camera/cameraInterface";
import { SavedView } from "@features/db/entities/savedView";
import { useEditorContext } from "@features/editor/hooks/useEditorContext";
import { useCallback } from "react";

type SavedViewsToolbarProps = {
savedViews?: SavedView[];
embedMode?: boolean;
};

export default function SavedViewsToolbar({ savedViews = [], embedMode = false }: SavedViewsToolbarProps) {
const { renderer, activeView } = useEditorContext();

const handleSelectionChange = useCallback(
(selectedKey: string | number) => {
if (!selectedKey) return;

const selectedView = savedViews.find((view) => view.id.toString() === selectedKey.toString());

if (!selectedView) return;

// Set camera position in the editor context
const currentView = renderer.views?.[activeView];
if (!currentView) {
console.error("No active view found");
return;
}

// Set camera position and target to the saved view values
currentView.view.camera.set({
position: selectedView.cameraPosition,
target: selectedView.cameraTarget,
projectionType: selectedView.projectionType,
fovYRadian: selectedView.fovYRadian,
});

// For orthographic projection, set the saved orthographic bounds
if (selectedView.projectionType === ProjectionType.ORTHOGRAPHIC) {
currentView.view.camera.setOrthographicBounds(
selectedView.orthographicLeft,
selectedView.orthographicRight,
selectedView.orthographicBottom,
selectedView.orthographicTop,
);
}

// Update the camera matrices to reflect the new position
currentView.view.camera.updateProjectionViewMatrix();

console.log("Loaded saved view:", selectedView.name);
},
[renderer.views, activeView, savedViews],
);

// Only show in embed mode and if there are saved views
if (!embedMode || savedViews.length === 0) {
return null;
}

return (
<View
backgroundColor="gray-50"
padding="size-50"
borderRadius="medium"
borderColor="gray-300"
borderWidth="thin"
gridArea="savedViews"
>
<TooltipTrigger delay={0} placement="bottom">
<Picker onSelectionChange={handleSelectionChange} isQuiet placeholder="Saved Views" items={savedViews}>
{(item) => (
<Item key={item.id.toString()} textValue={item.name}>
{item.name}
</Item>
)}
</Picker>
<Tooltip>Select saved view</Tooltip>
</TooltipTrigger>
</View>
);
}
Loading