Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/build-converter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ jobs:
with:
push: true
context: ./coordinates_converter
tags: "ghcr.io/metacitytools/coordinates-converter:latest"
tags: "ghcr.io/stdio-cz/mc-coordinates-converter:latest"
cache-from: type=gha
cache-to: type=gha,mode=max
2 changes: 1 addition & 1 deletion .github/workflows/build-studio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ jobs:
with:
push: true
context: ./studio
tags: "ghcr.io/metacitytools/studio:latest"
tags: "ghcr.io/stdio-cz/mc-studio:latest"
cache-from: type=gha
cache-to: type=gha,mode=max
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
## [1.2.0](https://github.com/stdio-cz/MetacityStudioLegacy/compare/v1.1.0...v1.2.0) (2025-06-23)

### Features

* add save views api and change layout ([8e2569e](https://github.com/stdio-cz/MetacityStudioLegacy/commit/8e2569e082b224f043f76aa6574da6df294642f7))
* add screenshot function and bookmark views (layout) ([596f0b6](https://github.com/stdio-cz/MetacityStudioLegacy/commit/596f0b635973cac11fa4af2a48484ba5fd39216f))
* save views persisted and loaded correctly with zoom ([5f3c2d4](https://github.com/stdio-cz/MetacityStudioLegacy/commit/5f3c2d4a0d70c4bfc228b2a816d5d5e82baf83c4))
* update tooltip rendering ([0201a90](https://github.com/stdio-cz/MetacityStudioLegacy/commit/0201a90e045e86b8774daf547a5220a5ef904921))
## [1.1.0](https://github.com/stdio-cz/MetacityStudioLegacy/compare/v1.0.6...v1.1.0) (2025-06-10)

### Features

* add property only tooltip info to embeds (with persistence to DB and GUI) ([d54d01d](https://github.com/stdio-cz/MetacityStudioLegacy/commit/d54d01dc1ebd72ef04d75e99932c568af6c7284b))
* allow hover over tooltip ([fc7e8c8](https://github.com/stdio-cz/MetacityStudioLegacy/commit/fc7e8c8d543d1c4b486c6726fc153cd44423d956))
* format urls in tooltip ([16352f3](https://github.com/stdio-cz/MetacityStudioLegacy/commit/16352f3092ca5634574e9bb8ecf5810b2107128c))
* in embed viewer keep just free camera and top view ([6925b1e](https://github.com/stdio-cz/MetacityStudioLegacy/commit/6925b1e84c51ef679ae6e00156bc4b1c7eebb1cb))
* modify tooltip to show multiple column information and hide column selector ([e5a5b49](https://github.com/stdio-cz/MetacityStudioLegacy/commit/e5a5b493c5ed7dc4d2debdad87b5618914950cad))
* remove active column dropdown if only one and forbid change of column name in the dropdown ([5e17be9](https://github.com/stdio-cz/MetacityStudioLegacy/commit/5e17be988a2d98d08f714f3ffc9ef962c30d6f79))
## [1.0.6](https://github.com/stdio-cz/MetacityStudioLegacy/compare/v1.0.5...v1.0.6) (2025-02-03)

### Bug Fixes

* enable embeds viewing without authorization ([#4](https://github.com/stdio-cz/MetacityStudioLegacy/issues/4)) ([8dc5639](https://github.com/stdio-cz/MetacityStudioLegacy/commit/8dc5639754e6438076a9b31da4513f956a59b1c8))
## [1.0.5](https://github.com/stdio-cz/MetacityStudioLegacy/compare/v1.0.4...v1.0.5) (2025-02-03)

### Features

* set timeout 5s for every toaster ([#1](https://github.com/stdio-cz/MetacityStudioLegacy/issues/1)) ([408640b](https://github.com/stdio-cz/MetacityStudioLegacy/commit/408640bdd63b66c1e3f3d04554450f4260bb8d9d))

### Bug Fixes

* add disableShift parameter while spliting models ([#2](https://github.com/stdio-cz/MetacityStudioLegacy/issues/2)) ([16dbefb](https://github.com/stdio-cz/MetacityStudioLegacy/commit/16dbefbf7a40fb98f056fbe570c5b30931d5055d))
* changed hardcoded email address ([#3](https://github.com/stdio-cz/MetacityStudioLegacy/issues/3)) ([f545a4c](https://github.com/stdio-cz/MetacityStudioLegacy/commit/f545a4c59db88c502221bd0d8318845bb3372289))
## [1.0.4](https://github.com/MetacityTools/Studio/compare/v1.0.3...v1.0.4) (2024-09-20)

### Bug Fixes
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,17 @@ This repository is set up to be used with the Visual Studio Code Remote - Contai
The devcontainer includes:
* Node.js 20
* Python 3.12
* Docker-in-Docker
* Docker-in-Docker

After running the devcontainer:

```bash
npm run migrations:run
```

In case of an error after logging in, for MacOS users, comment out


```typescript
migrations: Config.environment === "test" ? undefined : ["features/db/migrations/*.ts"],
```
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "metacity",
"displayName": "MetaCity Studio",
"private": true,
"version": "1.0.4",
"version": "1.2.0",
"description": "",
"scripts": {
"version": "npm run version:changelog && npm run version:sync && git add -A",
Expand Down
33 changes: 30 additions & 3 deletions studio/app/api/embeds/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,50 @@ const postSchema = zfd.formData({
thumbnailFileContents: zfd.text(),
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" },
},
);
}
}
80 changes: 80 additions & 0 deletions studio/app/api/savedViews/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ProjectionType } from "@features/bananagl/camera/cameraInterface";
import { SavedView } from "@features/db/entities/savedView";
import { injectRepository } from "@features/db/helpers";
import { z } from "zod";

const putSchema = z.object({
name: z.string().min(1).optional(),
cameraPosition: z.array(z.number()).length(3).optional(),
cameraTarget: z.array(z.number()).length(3).optional(),
projectionType: z.nativeEnum(ProjectionType).optional(),
fovYRadian: z.number().optional(),
orthographicLeft: z.number().optional(),
orthographicRight: z.number().optional(),
orthographicBottom: z.number().optional(),
orthographicTop: z.number().optional(),
});

export async function GET(req: Request, { params }: { params: { id: string } }) {
try {
const savedViewRepository = await injectRepository(SavedView);
const savedView = await savedViewRepository.findOne({
where: { id: parseInt(params.id) },
});

if (!savedView) {
return new Response("Saved view not found", { status: 404 });
}

return Response.json(savedView);
} catch (e) {
console.error("Error fetching saved view:", e);
return new Response("Internal server error", { status: 500 });
}
}

export async function PUT(req: Request, { params }: { params: { id: string } }) {
try {
const body = await req.json();
const data = putSchema.parse(body);

const savedViewRepository = await injectRepository(SavedView);
const savedView = await savedViewRepository.findOne({
where: { id: parseInt(params.id) },
});

if (!savedView) {
return new Response("Saved view not found", { status: 404 });
}

Object.assign(savedView, data);
const result = await savedViewRepository.save(savedView);

return Response.json(result);
} catch (e) {
if (e instanceof z.ZodError) {
return new Response(e.message, { status: 400 });
}
console.error("Error updating saved view:", e);
return new Response("Internal server error", { status: 500 });
}
}

export async function DELETE(req: Request, { params }: { params: { id: string } }) {
try {
const savedViewRepository = await injectRepository(SavedView);
const savedView = await savedViewRepository.findOne({
where: { id: parseInt(params.id) },
});

if (!savedView) {
return new Response("Saved view not found", { status: 404 });
}

await savedViewRepository.remove(savedView);
return new Response(null, { status: 204 });
} catch (e) {
console.error("Error deleting saved view:", e);
return new Response("Internal server error", { status: 500 });
}
}
69 changes: 69 additions & 0 deletions studio/app/api/savedViews/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ProjectionType } from "@features/bananagl/camera/cameraInterface";
import { SavedView } from "@features/db/entities/savedView";
import { injectRepository } from "@features/db/helpers";
import { z } from "zod";

const postSchema = z.object({
name: z.string().min(1),
cameraPosition: z.array(z.number()).length(3),
cameraTarget: z.array(z.number()).length(3),
projectionType: z.nativeEnum(ProjectionType),
fovYRadian: z.number(),
orthographicLeft: z.number(),
orthographicRight: z.number(),
orthographicBottom: z.number(),
orthographicTop: z.number(),
projectId: z.number(),
});

export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const projectId = searchParams.get("projectId");

if (!projectId) {
return new Response("Project ID is required", { status: 400 });
}

const savedViewRepository = await injectRepository(SavedView);
const savedViews = await savedViewRepository.find({
where: { project: { id: parseInt(projectId) } },
order: { created_at: "DESC" },
});

return Response.json(savedViews);
} catch (e) {
console.error("Error fetching saved views:", e);
return new Response("Internal server error", { status: 500 });
}
}

export async function POST(req: Request) {
try {
const body = await req.json();
const data = postSchema.parse(body);

const savedViewRepository = await injectRepository(SavedView);
const savedView = savedViewRepository.create({
name: data.name,
cameraPosition: data.cameraPosition,
cameraTarget: data.cameraTarget,
projectionType: data.projectionType,
fovYRadian: data.fovYRadian,
orthographicLeft: data.orthographicLeft,
orthographicRight: data.orthographicRight,
orthographicBottom: data.orthographicBottom,
orthographicTop: data.orthographicTop,
project: { id: data.projectId },
});

const result = await savedViewRepository.save(savedView);
return Response.json(result, { status: 201 });
} catch (e) {
if (e instanceof z.ZodError) {
return new Response(e.message, { status: 400 });
}
console.error("Error creating saved view:", e);
return new Response("Internal server error", { status: 500 });
}
}
2 changes: 1 addition & 1 deletion studio/app/embeds/[embedId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ function EmbedPage({ params }: ProjectPageProps) {
);
}

export default withPageAuthRequired(withUserEnabled(EmbedPage));
export default EmbedPage;
2 changes: 1 addition & 1 deletion studio/app/user-not-enabled/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function UserNotEnabledPage() {

<p>
Your account has not been enabled. Please contact an administrator at{" "}
<a href="mailto:vojta@stdio.cz">vojta@stdio.cz</a>
<a href="mailto:hi@stdio.cz">hi@stdio.cz</a>
</p>
</View>
</Grid>
Expand Down
5 changes: 5 additions & 0 deletions studio/core/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SpectrumToastOptions } from "@react-spectrum/toast";

export const toasterOptions: SpectrumToastOptions = {
timeout: 5000,
};
13 changes: 13 additions & 0 deletions studio/core/icons/MdiBookmark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Icon as AdobeIcon } from "@adobe/react-spectrum";
import { IconProps } from "./Icon";

export function MdiBookmark(props: IconProps) {
const { transform, ...rest } = props;
return (
<AdobeIcon {...rest}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" d="M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z" />
</svg>
</AdobeIcon>
);
}
16 changes: 16 additions & 0 deletions studio/core/icons/MdiCamera.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Icon as AdobeIcon } from "@adobe/react-spectrum";
import { IconProps } from "./Icon";

export function MdiCamera(props: IconProps) {
const { transform, ...rest } = props;
return (
<AdobeIcon {...rest}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M4 4h3l2-2h6l2 2h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2m8 3a5 5 0 0 0-5 5a5 5 0 0 0 5 5a5 5 0 0 0 5-5a5 5 0 0 0-5-5m0 2a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3"
/>
</svg>
</AdobeIcon>
);
}
31 changes: 29 additions & 2 deletions studio/features/api-sdk/uploadEmbed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,42 @@ export default async function uploadEmbed(
dataFile: File,
thumbnailFileContents: string,
name: string,
onlyTooltipInfo?: boolean,
savedViewIds?: number[],
) {
const formData = new FormData();

formData.append("projectId", projectId.toString());
formData.append("dataFile", dataFile);
formData.append("thumbnailFileContents", thumbnailFileContents);
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;
}
}
Loading