Skip to content

Commit 83f838e

Browse files
committed
feat: adds initial file drag and drop functionality
1 parent 696c0e0 commit 83f838e

8 files changed

Lines changed: 359 additions & 7 deletions

File tree

apps/vscode/src/extension/editor/document.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,27 @@ export class TriplexDocument implements vscode.CustomDocument {
208208
});
209209
}
210210

211+
async insertComponent(data: { scenePath: string; componentPath: string }) {
212+
return this.undoableAction("Insert component", async () => {
213+
const result = await fetch(
214+
`http://localhost:${
215+
this._context.ports.server
216+
}/scene/${encodeURIComponent(data.scenePath)}/add-component`,
217+
{
218+
method: "POST",
219+
headers: {
220+
"Content-Type": "application/json",
221+
},
222+
body: JSON.stringify({
223+
componentPath: data.componentPath,
224+
}),
225+
},
226+
);
227+
const response: Mutation = await result.json();
228+
return { ...response, path: data.scenePath };
229+
});
230+
}
231+
211232
async moveElement(data: {
212233
action: "move-before" | "move-after" | "make-child" | "reparent";
213234
destination: { astPath: string; column: number; line: number };

apps/vscode/src/extension/editor/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ export class TriplexEditorProvider
187187
sendVSCE(panel.webview, "vscode:request-blur-element", undefined);
188188
await document.deleteElement(element);
189189
}),
190+
on(panel.webview, "component-insert", async (data) => {
191+
await document.insertComponent(data);
192+
}),
190193
on(panel.webview, "notification", async (data) => {
191194
switch (data.type) {
192195
case "info":
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright (c) 2022—present Michael Dougall. All rights reserved.
3+
*
4+
* This repository utilizes multiple licenses across different directories. To
5+
* see this files license find the nearest LICENSE file up the source tree.
6+
*/
7+
import { MapControls, PerspectiveCamera } from "@react-three/drei";
8+
9+
export function DropScene({
10+
name: ___ = "jelly",
11+
value: __ = 100,
12+
variant: _ = "giant",
13+
visible: ____ = true,
14+
}: SceneProps) {
15+
return (
16+
<>
17+
<ambientLight position={[2.12, 0, -0.88]} />
18+
19+
<PerspectiveCamera
20+
makeDefault={true}
21+
name={"user_defined"}
22+
position={[-1.831_592_557_014_578_1, 0.943_661_973_904_340_8, 0]}
23+
rotation={[
24+
-1.570_796_326_794_896_6, -1.221_730_476_396_030_6,
25+
-1.570_796_326_794_896_6,
26+
]}
27+
/>
28+
<group>
29+
<mesh position={[1.6, 0, -1.7]}>
30+
<boxGeometry />
31+
<meshStandardMaterial />
32+
</mesh>
33+
<mesh position={[0.04, 0, -1.7]} scale={[2.42, 2.24, 1]}>
34+
<boxGeometry />
35+
<meshStandardMaterial />
36+
</mesh>
37+
</group>
38+
39+
<MapControls makeDefault={false} />
40+
</>
41+
);
42+
}
43+
44+
interface SceneProps {
45+
name?: string;
46+
/**
47+
* @min 1
48+
* @max 10
49+
* @step 0.5
50+
*/
51+
value?: number;
52+
variant?: "giant" | "small";
53+
visible?: boolean;
54+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) 2022—present Michael Dougall. All rights reserved.
3+
*
4+
* This repository utilizes multiple licenses across different directories. To
5+
* see this files license find the nearest LICENSE file up the source tree.
6+
*/
7+
import { useState } from "react";
8+
import { type Vector3Tuple } from "three";
9+
10+
export const DropBox = ({
11+
children,
12+
color = "blue",
13+
position,
14+
rotation,
15+
scale,
16+
size = 1,
17+
}: {
18+
children?: React.ReactNode;
19+
color?: "red" | "green" | "blue";
20+
position?: Vector3Tuple | number;
21+
rotation?: Vector3Tuple;
22+
scale?: Vector3Tuple | number;
23+
/**
24+
* @min 1
25+
* @max 2
26+
* @step 0.1
27+
* @group Custom Group
28+
*/
29+
size?: number | string;
30+
}) => {
31+
const ok = {};
32+
const [hover, setHover] = useState(false);
33+
return (
34+
<group scale={scale} visible={true}>
35+
<mesh
36+
{...ok}
37+
name="hello-world"
38+
onClick={() => {}}
39+
onPointerOut={() => setHover(false)}
40+
onPointerOver={() => setHover(true)}
41+
position={position}
42+
rotation={rotation}
43+
userData={{ hello: true }}
44+
visible={true}
45+
>
46+
<boxGeometry args={[size, size, size]} />
47+
<meshStandardMaterial color={hover ? "purple" : color} key={color} />
48+
<meshBasicMaterial color="green" />
49+
{children}
50+
</mesh>
51+
</group>
52+
);
53+
};

packages/@triplex/editor-next/src/features/app-root/index.tsx

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,66 @@
66
*/
77
import { fg } from "@triplex/lib/fg";
88
import { useScreenView } from "@triplex/ux";
9+
import { DragEvent, useRef, useState } from "react";
910
import { preloadSubscription } from "../../hooks/ws";
11+
import { sendVSCE } from "../../util/bridge";
1012
import { AIChat } from "../ai-chat";
1113
import { FloatingControls } from "../floating-controls";
1214
import { Panels } from "../panels";
15+
import { useSceneContext } from "./context";
1316
import { Dialogs } from "./dialogs";
1417
import { EmptyState } from "./empty-state";
1518
import { Events } from "./events";
1619

1720
export function AppRoot() {
1821
useScreenView("app", "Screen");
1922

23+
const context = useSceneContext();
24+
const [isDragging, setIsDragging] = useState(false);
25+
const dropZoneRef = useRef<HTMLDivElement>(null);
26+
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
27+
e.preventDefault();
28+
e.stopPropagation();
29+
};
30+
const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {
31+
e.preventDefault();
32+
e.stopPropagation();
33+
setIsDragging(true);
34+
};
35+
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {
36+
e.preventDefault();
37+
e.stopPropagation();
38+
39+
// Only set to false if leaving the drop zone itself
40+
if (
41+
e.currentTarget === dropZoneRef.current &&
42+
!dropZoneRef.current.contains(e.relatedTarget as Node)
43+
) {
44+
setIsDragging(false);
45+
}
46+
};
47+
48+
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
49+
e.preventDefault();
50+
e.stopPropagation();
51+
setIsDragging(false);
52+
const items = e.dataTransfer.items;
53+
const fileUris: string[] = [];
54+
55+
for (let i = 0; i < items.length; i++) {
56+
const item = items[i];
57+
if (item.kind === "string" && item.type === "text/uri-list") {
58+
item.getAsString((uri) => {
59+
fileUris.push(uri);
60+
sendVSCE("component-insert", {
61+
componentPath: uri,
62+
scenePath: context.path,
63+
});
64+
});
65+
}
66+
}
67+
};
68+
2069
return (
2170
<div className="fixed inset-0 flex select-none">
2271
<Events />
@@ -25,13 +74,53 @@ export function AppRoot() {
2574
<div className="relative h-full w-full">
2675
<FloatingControls />
2776
<EmptyState />
28-
<iframe
29-
allow="cross-origin-isolated"
30-
className="h-full w-full"
31-
data-testid="scene"
32-
id="scene"
33-
src={`http://localhost:${window.triplex.env.ports.client}/scene`}
34-
/>
77+
<div
78+
ref={dropZoneRef}
79+
onDragOver={handleDragOver}
80+
onDragEnter={handleDragEnter}
81+
onDragLeave={handleDragLeave}
82+
onDrop={handleDrop}
83+
style={{
84+
position: "relative",
85+
height: "100%",
86+
width: "100%",
87+
padding: "16px",
88+
}}
89+
>
90+
{isDragging && (
91+
<div
92+
style={{
93+
border: "2px dashed #ccc",
94+
borderRadius: "8px",
95+
padding: "40px",
96+
textAlign: "center",
97+
backgroundColor: "#f5f5f5",
98+
transition: "all 0.3s ease",
99+
minHeight: "200px",
100+
display: "flex",
101+
flexDirection: "column",
102+
justifyContent: "center",
103+
alignItems: "center",
104+
position: "absolute",
105+
top: "0",
106+
left: "0",
107+
width: "100%",
108+
height: "100%",
109+
zIndex: "99",
110+
pointerEvents: "all",
111+
}}
112+
>
113+
<p>Drop Component here</p>
114+
</div>
115+
)}
116+
<iframe
117+
allow="cross-origin-isolated"
118+
className="h-full w-full"
119+
data-testid="scene"
120+
id="scene"
121+
src={`http://localhost:${window.triplex.env.ports.client}/scene`}
122+
/>
123+
</div>
35124
</div>
36125
{fg("ai_chat") && <AIChat />}
37126
</div>

packages/@triplex/editor-next/src/util/bridge.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ export interface ToVSCodeEvent extends ClientSendEventData {
118118
terminal: {
119119
command: string;
120120
};
121+
"component-insert": {
122+
scenePath: string;
123+
componentPath: string;
124+
};
121125
}
122126

123127
/**

packages/@triplex/server/src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* see this files license find the nearest LICENSE file up the source tree.
66
*/
77
import { readFile } from "node:fs/promises";
8+
import { fileURLToPath } from "node:url";
89
import { Application, isHttpError, Router } from "@oakserver/oak";
910
import { fg, initFeatureGates } from "@triplex/lib/fg";
1011
import { createForkLogger } from "@triplex/lib/log";
@@ -32,6 +33,7 @@ import { propGroupsDef } from "./ast/prop-groupings";
3233
import { createAI } from "./services/ai";
3334
import {
3435
add,
36+
addComponentToEnd,
3537
commentComponent,
3638
create,
3739
deleteElement,
@@ -341,6 +343,21 @@ export async function createServer({
341343
context.response.body = { ...ids };
342344
});
343345

346+
router.post("/scene/:path/add-component", async (context) => {
347+
const { path: scenePath } = context.params;
348+
349+
const body = await context.request.body().value;
350+
351+
const componentPath = fileURLToPath(body.componentPath);
352+
353+
const sceneFile = project.getSourceFile(scenePath);
354+
355+
const [ids] = await sceneFile.edit((source) => {
356+
addComponentToEnd(source, componentPath);
357+
});
358+
context.response.body = { ...ids, success: true };
359+
});
360+
344361
router.post("/scene/new", (context) => {
345362
const exportName = "Untitled";
346363
const sourceFile = project.createSourceFile(exportName);

0 commit comments

Comments
 (0)