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
67 changes: 40 additions & 27 deletions packages/chili/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,31 +83,13 @@ export class Application implements IApplication {

private initWindowEvents() {
window.onbeforeunload = this.handleWindowUnload;
this.mainWindow?.addEventListener(
"dragstart",
(ev) => {
ev.preventDefault();
},
false,
);
this.mainWindow?.addEventListener(
"dragover",
(ev) => {
ev.stopPropagation();
ev.preventDefault();
ev.dataTransfer!.dropEffect = "copy";
},
false,
);
this.mainWindow?.addEventListener(
"drop",
(ev) => {
ev.stopPropagation();
ev.preventDefault();
this.importFiles(ev.dataTransfer?.files);
},
false,
);
this.mainWindow?.addEventListener("dragstart", this.handleDragStart, false);
// Use capture to intercept file-drop before nested widgets call stopPropagation().
this.mainWindow?.addEventListener("dragover", this.handleDragOver, true);
this.mainWindow?.addEventListener("drop", this.handleDrop, true);
// Fallback: catch drag/drop anywhere in page, also in capture phase.
window.addEventListener("dragover", this.handleDragOver, true);
window.addEventListener("drop", this.handleDrop, true);
}

private readonly handleWindowUnload = (event: BeforeUnloadEvent) => {
Expand All @@ -119,6 +101,25 @@ export class Application implements IApplication {
}
};

private readonly handleDragStart = (ev: DragEvent) => {
ev.preventDefault();
};

private readonly handleDragOver = (ev: DragEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (ev.dataTransfer) {
ev.dataTransfer.dropEffect = "copy";
}
};

private readonly handleDrop = (ev: DragEvent) => {
ev.stopPropagation();
ev.preventDefault();
const files = this.extractDroppedFiles(ev.dataTransfer);
this.importFiles(files);
};

async importFiles(files: File[] | FileList | undefined) {
if (!files || files.length === 0) {
return;
Expand Down Expand Up @@ -162,9 +163,10 @@ export class Application implements IApplication {
const imports: File[] = [];
const plugins: File[] = [];
for (const element of files) {
if (element.name.endsWith(DOCUMENT_FILE_EXTENSION)) {
const fileName = element.name.toLowerCase();
if (fileName.endsWith(DOCUMENT_FILE_EXTENSION)) {
opens.push(element);
} else if (element.name.endsWith(PLUGIN_FILE_EXTENSION)) {
} else if (fileName.endsWith(PLUGIN_FILE_EXTENSION)) {
plugins.push(element);
} else {
imports.push(element);
Expand All @@ -173,6 +175,17 @@ export class Application implements IApplication {
return { opens, imports, plugins };
}

private extractDroppedFiles(dataTransfer: DataTransfer | null): File[] {
if (!dataTransfer) return [];
const fromFileList = Array.from(dataTransfer.files ?? []);
if (fromFileList.length > 0) return fromFileList;
const fromItems = Array.from(dataTransfer.items ?? [])
.filter((item) => item.kind === "file")
.map((item) => item.getAsFile())
.filter((file): file is File => file !== null);
return fromItems;
}

async openDocument(id: string): Promise<IDocument | undefined> {
const document = await Document.open(this, id);
await this.createActiveView(document);
Expand Down
2 changes: 2 additions & 0 deletions plugins/helloworld-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Package the plugin (creates .chiliplugin file):
npm run package
```

The packaging script is cross-platform and works on both Windows (PowerShell) and macOS/Linux.

## Installation

Drag and drop the .chiliplugin file into Chili3D.
2 changes: 1 addition & 1 deletion plugins/helloworld-js/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Hello World JS",
"version": "1.0.0",
"description": "A demo plugin for Chili3D showing plugin system capabilities",
"main": "src/extension.js",
"main": "extension.js",
"author": {
"name": "Chili3D Team"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/helloworld-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "extension.js",
"type": "module",
"scripts": {
"package": "rm -rf dist && mkdir dist && zip -r ./dist/helloworld-js.chiliplugin manifest.json ./src/extension.js icons"
"package": "node ../scripts/package-plugin.mjs --entry src/extension.js --output dist/helloworld-js.chiliplugin"
},
"keywords": [
"chili3d",
Expand Down
5 changes: 1 addition & 4 deletions plugins/helloworld-js/src/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ class HelloWorldCommand {

CommandStore.registerCommand(HelloWorldCommand, {
key: "demo.hello",
icon: {
type: "plugin",
path: "icons/hello.svg",
},
icon: "icon-info",
});

const DemoPlugin = {
Expand Down
2 changes: 2 additions & 0 deletions plugins/helloworld-ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ npm run build
npm run package
```

The packaging script is cross-platform and works on both Windows (PowerShell) and macOS/Linux.

## Installation

Drag and drop the `.chiliplugin` file into Chili3D.
2 changes: 1 addition & 1 deletion plugins/helloworld-ts/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Demo Plugin",
"version": "1.0.0",
"description": "A demo plugin for Chili3D showing plugin system capabilities",
"main": "dist/extension.js",
"main": "extension.js",
"author": {
"name": "Chili3D Team"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/helloworld-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"build": "rspack build",
"package": "npm run build && zip -r ./dist/helloworld-ts.chiliplugin manifest.json ./dist/extension.js icons"
"package": "npm run build && node ../scripts/package-plugin.mjs --entry dist/extension.js --output dist/helloworld-ts.chiliplugin"
},
"devDependencies": {
"chili-api": "*",
Expand Down
5 changes: 1 addition & 4 deletions plugins/helloworld-ts/src/commands/hello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { command, type I18nKeys, type IApplication, type ICommand, PubSub } from

@command({
key: "demo.hello" as any,
icon: {
type: "plugin",
path: "icons/hello.svg",
},
icon: "icon-info",
helpText: "demo.hello.description" as any,
})
export class HelloWorldCommand implements ICommand {
Expand Down
72 changes: 72 additions & 0 deletions plugins/scripts/package-plugin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Part of the Chili3d Project, under the AGPL-3.0 License.
// See LICENSE file in the project root for full license information.

import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import JSZip from "jszip";

function parseArgs(argv) {
const args = new Map();
for (let i = 0; i < argv.length; i += 2) {
const key = argv[i];
const value = argv[i + 1];
if (!key?.startsWith("--") || value === undefined) {
continue;
}
args.set(key.slice(2), value);
}
return {
entry: args.get("entry"),
output: args.get("output"),
};
}

async function addDirectory(zip, rootDir, zipDir) {
const entries = await readdir(rootDir, { withFileTypes: true });
for (const entry of entries) {
const abs = path.join(rootDir, entry.name);
const zipPath = `${zipDir}/${entry.name}`;
if (entry.isDirectory()) {
await addDirectory(zip, abs, zipPath);
continue;
}
if (entry.isFile()) {
const content = await readFile(abs);
zip.file(zipPath, content);
}
}
}

async function exists(filePath) {
try {
await stat(filePath);
return true;
} catch {
return false;
}
}

async function main() {
const { entry, output } = parseArgs(process.argv.slice(2));
if (!entry || !output) {
throw new Error("Missing required args: --entry <path> --output <path>");
}

const zip = new JSZip();
zip.file("manifest.json", await readFile("manifest.json"));
zip.file("extension.js", await readFile(entry));

if (await exists("icons")) {
await addDirectory(zip, "icons", "icons");
}

await rm("dist", { recursive: true, force: true });
await mkdir(path.dirname(output), { recursive: true });
const data = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
await writeFile(output, data);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});