mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 06:27:04 +01:00
Compare commits
16 Commits
v0.10.0-rc
...
v0.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6deef9e7f | ||
|
|
ad956762f4 | ||
|
|
a4acd2f3d3 | ||
|
|
c877fef490 | ||
|
|
0855a9c6c7 | ||
|
|
657b34d098 | ||
|
|
ad83f1c937 | ||
|
|
38be4c638b | ||
|
|
63f2b716d6 | ||
|
|
9e70a3998d | ||
|
|
c7c4adc250 | ||
|
|
393decd876 | ||
|
|
111f417905 | ||
|
|
7296b15a67 | ||
|
|
88190b0d1e | ||
|
|
f2a607bb00 |
@@ -15,15 +15,17 @@ in a web browser.
|
|||||||
- Select any entity and measure bounding box size and distances.
|
- Select any entity and measure bounding box size and distances.
|
||||||
- Hot reloading while editing the CAD model (using the `yacv-server` package).
|
- Hot reloading while editing the CAD model (using the `yacv-server` package).
|
||||||
- Fully-featured static deployment: just upload the viewer and models to your server.
|
- Fully-featured static deployment: just upload the viewer and models to your server.
|
||||||
|
- Build123d playground! code and build your model fully inside the
|
||||||
|
browser: [demo](https://yeicor-3d.github.io/yet-another-cad-viewer/#pg_code_url=https://raw.githubusercontent.com/gumyr/build123d/refs/heads/dev/examples/toy_truck.py).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
The [example](example) is a fully working project that shows how to use the viewer.
|
The [example](example) is a fully working project that shows how to use the viewer.
|
||||||
|
|
||||||
You can play with the latest
|
You can play with the latest
|
||||||
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
|
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
|
||||||
(or
|
(or
|
||||||
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
|
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -32,4 +34,5 @@ demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb
|
|||||||
- [cq-studio](https://github.com/ccazabon/cq-studio) provides an alternative workflow that detects file changes instead
|
- [cq-studio](https://github.com/ccazabon/cq-studio) provides an alternative workflow that detects file changes instead
|
||||||
of relying on an interactive environment like Jupyter for hot-reloading.
|
of relying on an interactive environment like Jupyter for hot-reloading.
|
||||||
Uses the same backend and frontend behind the scenes.
|
Uses the same backend and frontend behind the scenes.
|
||||||
- [build123d-docker](https://github.com/derhuerst/build123d-docker/pkgs/container/build123d) provides docker images for Yet Another CAD Viewer and other projects, with automatic updates.
|
- [build123d-docker](https://github.com/derhuerst/build123d-docker/pkgs/container/build123d) provides docker images for
|
||||||
|
Yet Another CAD Viewer and other projects, with automatic updates.
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
|||||||
let model = event.models[modelIndex];
|
let model = event.models[modelIndex];
|
||||||
tools.value?.removeObjectSelections(model.name);
|
tools.value?.removeObjectSelections(model.name);
|
||||||
try {
|
try {
|
||||||
let loadHelpers = (await settings()).loadHelpers;
|
let loadHelpers = (await settings).loadHelpers;
|
||||||
if (!model.isRemove) {
|
if (!model.isRemove) {
|
||||||
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && loadHelpers, isLast);
|
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && loadHelpers, isLast);
|
||||||
} else {
|
} else {
|
||||||
@@ -83,7 +83,7 @@ networkMgr.addEventListener('update-early',
|
|||||||
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
|
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
|
||||||
let preloadingModels = ref<Array<string>>([]);
|
let preloadingModels = ref<Array<string>>([]);
|
||||||
(async () => { // Start loading all configured models ASAP
|
(async () => { // Start loading all configured models ASAP
|
||||||
let sett = await settings();
|
let sett = await settings;
|
||||||
if (sett.preload.length > 0) {
|
if (sett.preload.length > 0) {
|
||||||
watch(viewer, (newViewer) => {
|
watch(viewer, (newViewer) => {
|
||||||
if (newViewer) {
|
if (newViewer) {
|
||||||
@@ -108,6 +108,23 @@ async function loadModelManual() {
|
|||||||
const modelUrl = prompt("For an improved experience in viewing CAD/GLTF models with automatic updates, it's recommended to use the official yacv_server Python package. This ensures seamless serving of models and automatic updates.\n\nOtherwise, enter the URL of the model to load:");
|
const modelUrl = prompt("For an improved experience in viewing CAD/GLTF models with automatic updates, it's recommended to use the official yacv_server Python package. This ensures seamless serving of models and automatic updates.\n\nOtherwise, enter the URL of the model to load:");
|
||||||
if (modelUrl) await networkMgr.load(modelUrl);
|
if (modelUrl) await networkMgr.load(modelUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect dropped .glb files and load them manually
|
||||||
|
document.body.addEventListener("dragover", e => {
|
||||||
|
e.preventDefault(); // Allow drop
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener("drop", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = e.dataTransfer?.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
if (ext === 'glb' || ext === 'gltf') {
|
||||||
|
await networkMgr.load(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ let isSmallBuild = typeof __YACV_SMALL_BUILD__ !== 'undefined' && __YACV_SMALL_B
|
|||||||
*
|
*
|
||||||
* Remember to call mergeFinalize after all models have been merged (slower required operations).
|
* Remember to call mergeFinalize after all models have been merged (slower required operations).
|
||||||
*/
|
*/
|
||||||
export async function mergePartial(url: string, name: string, document: Document, networkFinished: () => void = () => {
|
export async function mergePartial(url: string | Blob, name: string, document: Document, networkFinished: () => void = () => {
|
||||||
}): Promise<Document> {
|
}): Promise<Document> {
|
||||||
// Fetch the complete document from the network
|
// Fetch the complete document from the network
|
||||||
// This could be done at the same time as the document is being processed, but I wanted better metrics
|
// This could be done at the same time as the document is being processed, but I wanted better metrics
|
||||||
let response = await fetch(url);
|
let response = await fetchOrRead(url);
|
||||||
let buffer = await response.arrayBuffer();
|
let buffer = await response.arrayBuffer();
|
||||||
networkFinished();
|
networkFinished();
|
||||||
|
|
||||||
@@ -118,3 +118,30 @@ function mergeScenes(): Transform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetches a URL or reads it if it is a Blob URL */
|
||||||
|
async function fetchOrRead(url: string | Blob) {
|
||||||
|
if (url instanceof Blob) {
|
||||||
|
// Use the FileReader API as fetch does not support Blob URLs
|
||||||
|
return new Promise<Response>((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = (event: ProgressEvent<FileReader>) => {
|
||||||
|
if (event.target && event.target.result) {
|
||||||
|
resolve(new Response(event.target.result));
|
||||||
|
} else {
|
||||||
|
reject(new Error("Failed to read Blob URL: " + url));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
reject(new Error("Error reading Blob URL: " + url + " - " + error));
|
||||||
|
};
|
||||||
|
// Read the Blob URL as an ArrayBuffer
|
||||||
|
reader.readAsArrayBuffer(new Blob([url]));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fetch the URL
|
||||||
|
return fetch(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ const batchTimeout = 250; // ms
|
|||||||
|
|
||||||
export class NetworkUpdateEventModel {
|
export class NetworkUpdateEventModel {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string | Blob;
|
||||||
// TODO: Detect and manage instances of the same object (same hash, different name)
|
// TODO: Detect and manage instances of the same object (same hash, different name)
|
||||||
hash: string | null;
|
hash: string | null;
|
||||||
isRemove: boolean | null; // This is null for a shutdown event
|
isRemove: boolean | null; // This is null for a shutdown event
|
||||||
|
|
||||||
constructor(name: string, url: string, hash: string | null, isRemove: boolean | null) {
|
constructor(name: string, url: string | Blob, hash: string | null, isRemove: boolean | null) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.hash = hash;
|
this.hash = hash;
|
||||||
@@ -42,18 +42,26 @@ export class NetworkManager extends EventTarget {
|
|||||||
*
|
*
|
||||||
* Updates will be emitted as "update" events, including the download URL and the model name.
|
* Updates will be emitted as "update" events, including the download URL and the model name.
|
||||||
*/
|
*/
|
||||||
async load(url: string) {
|
async load(url: string | Blob) {
|
||||||
if (url.startsWith("dev+") || url.startsWith("dev ")) {
|
if (url instanceof String && (url.startsWith("dev+") || url.startsWith("dev "))) {
|
||||||
let baseUrl = new URL(url.slice(4));
|
let baseUrl = new URL(url.slice(4));
|
||||||
baseUrl.searchParams.set("api_updates", "true");
|
baseUrl.searchParams.set("api_updates", "true");
|
||||||
await this.monitorDevServer(baseUrl);
|
await this.monitorDevServer(baseUrl);
|
||||||
} else {
|
} else {
|
||||||
// Get the last part of the URL as the "name" of the model
|
let name;
|
||||||
let name = url.split("/").pop();
|
let hash = null;
|
||||||
name = name?.split(".")[0] || `unknown-${Math.random()}`;
|
if (url instanceof Blob) {
|
||||||
// Use a head request to get the hash of the file
|
if (url instanceof File) name = (url as File).name
|
||||||
let response = await fetch(url, {method: "HEAD"});
|
else name = `blob-${Math.random()}`;
|
||||||
let hash = response.headers.get("etag");
|
name = name.replace('.glb', '').replace('.gltf', '');
|
||||||
|
} else {
|
||||||
|
// Get the last part of the URL as the "name" of the model
|
||||||
|
name = url.split("/").pop();
|
||||||
|
name = name?.split(".")[0] || `unknown-${Math.random()}`;
|
||||||
|
// Use a head request to get the hash of the file
|
||||||
|
let response = await fetch(url, {method: "HEAD"});
|
||||||
|
hash = response.headers.get("etag");
|
||||||
|
}
|
||||||
// Only trigger an update if the hash has changed
|
// Only trigger an update if the hash has changed
|
||||||
this.foundModel(name, hash, url, false);
|
this.foundModel(name, hash, url, false);
|
||||||
}
|
}
|
||||||
@@ -61,7 +69,7 @@ export class NetworkManager extends EventTarget {
|
|||||||
|
|
||||||
private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
|
private async monitorDevServer(url: URL, stop: () => boolean = () => false) {
|
||||||
while (!stop()) {
|
while (!stop()) {
|
||||||
let monitorEveryMs = (await settings()).monitorEveryMs;
|
let monitorEveryMs = (await settings).monitorEveryMs;
|
||||||
try {
|
try {
|
||||||
// WARNING: This will spam the console logs with failed requests when the server is down
|
// WARNING: This will spam the console logs with failed requests when the server is down
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -92,7 +100,7 @@ export class NetworkManager extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
|
private foundModel(name: string, hash: string | null, url: string | Blob, isRemove: boolean | null, disconnect: () => void = () => {
|
||||||
}) {
|
}) {
|
||||||
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
|
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {Matrix4} from "three/src/math/Matrix4.js"
|
|||||||
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
||||||
export class SceneMgr {
|
export class SceneMgr {
|
||||||
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
|
/** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */
|
||||||
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
|
static async loadModel(sceneUrl: Ref<string>, document: Document, name: string, url: string | Blob, updateHelpers: boolean = true, reloadScene: boolean = true): Promise<Document> {
|
||||||
let loadStart = performance.now();
|
let loadStart = performance.now();
|
||||||
let loadNetworkEnd: number;
|
let loadNetworkEnd: number;
|
||||||
|
|
||||||
@@ -100,7 +100,9 @@ export class SceneMgr {
|
|||||||
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
|
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
|
||||||
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
|
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
|
||||||
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
|
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
|
||||||
return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
|
let newDocument = await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene);
|
||||||
|
URL.revokeObjectURL(helpersUrl);
|
||||||
|
return newDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Serializes the current document into a GLB and updates the viewerSrc */
|
/** Serializes the current document into a GLB and updates the viewerSrc */
|
||||||
@@ -112,6 +114,7 @@ export class SceneMgr {
|
|||||||
let buffer = await toBuffer(document);
|
let buffer = await toBuffer(document);
|
||||||
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
||||||
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
||||||
|
if (sceneUrl.value.startsWith("blob:")) URL.revokeObjectURL(sceneUrl.value);
|
||||||
sceneUrl.value = URL.createObjectURL(blob);
|
sceneUrl.value = URL.createObjectURL(blob);
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// These are the default values for the settings, which are overridden below
|
// These are the default values for the settings, which are overridden below
|
||||||
import {ungzip} from "pako";
|
import {ungzip} from "pako";
|
||||||
import {b66Decode} from "../tools/b66.ts";
|
import {b64UrlDecode} from "../tools/b64.ts";
|
||||||
|
|
||||||
let settingsCache: any = null;
|
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
|
||||||
|
export const settings = (async () => {
|
||||||
export async function settings() {
|
|
||||||
if (settingsCache !== null) return settingsCache;
|
|
||||||
let settings = {
|
let settings = {
|
||||||
preload: [
|
preload: [
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -80,6 +78,12 @@ export async function settings() {
|
|||||||
// Get the default preload URL if not overridden (requires a fetch that is avoided if possible)
|
// Get the default preload URL if not overridden (requires a fetch that is avoided if possible)
|
||||||
for (let i = 0; i < settings.preload.length; i++) {
|
for (let i = 0; i < settings.preload.length; i++) {
|
||||||
let url = settings.preload[i];
|
let url = settings.preload[i];
|
||||||
|
// Ignore empty preload URLs to allow overriding default auto behavior
|
||||||
|
if (url === '') {
|
||||||
|
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
|
||||||
|
continue; // Skip this preload URL
|
||||||
|
}
|
||||||
|
// Handle special <auto> preload URL
|
||||||
if (url === '<auto>') {
|
if (url === '<auto>') {
|
||||||
if (settings.pg_code != "") { // <auto> means no preload URL if code is set
|
if (settings.pg_code != "") { // <auto> means no preload URL if code is set
|
||||||
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
|
settings.preload = settings.preload.slice(0, i).concat(settings.preload.slice(i + 1));
|
||||||
@@ -102,9 +106,9 @@ export async function settings() {
|
|||||||
// Auto-decompress the code and other playground settings
|
// Auto-decompress the code and other playground settings
|
||||||
if (settings.pg_code.length > 0) {
|
if (settings.pg_code.length > 0) {
|
||||||
try {
|
try {
|
||||||
settings.pg_code = ungzip(b66Decode(settings.pg_code), {to: 'string'});
|
settings.pg_code = ungzip(b64UrlDecode(settings.pg_code), {to: 'string'});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to decompress code (assuming raw code):", error);
|
console.log("pg_code is not base64url+gzipped, assuming raw code. Decoding error:", error);
|
||||||
}
|
}
|
||||||
if (settings.pg_opacity_loading < 0) {
|
if (settings.pg_opacity_loading < 0) {
|
||||||
// If the opacity is not set, use 0.0 if preload is set, otherwise 0.9
|
// If the opacity is not set, use 0.0 if preload is set, otherwise 0.9
|
||||||
@@ -112,11 +116,9 @@ export async function settings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsCache = settings;
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
})()
|
||||||
|
|
||||||
const firstTimeNames: Array<string> = []; // Needed for array values, which clear the array when overridden
|
|
||||||
function parseSetting(name: string, value: string, settings: any): any {
|
function parseSetting(name: string, value: string, settings: any): any {
|
||||||
let arrayElem = name.endsWith(".0")
|
let arrayElem = name.endsWith(".0")
|
||||||
if (arrayElem) name = name.slice(0, -2);
|
if (arrayElem) name = name.slice(0, -2);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const clipPlaneZ = ref(1);
|
|||||||
const clipPlaneSwappedZ = ref(false);
|
const clipPlaneSwappedZ = ref(false);
|
||||||
const edgeWidth = ref(0);
|
const edgeWidth = ref(0);
|
||||||
(async () => {
|
(async () => {
|
||||||
let s = await settings();
|
let s = await settings;
|
||||||
edgeWidth.value = s.edgeWidth;
|
edgeWidth.value = s.edgeWidth;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,34 @@ import {VueMonacoEditor} from '@guolao/vue-monaco-editor'
|
|||||||
import {nextTick, onMounted, ref, shallowRef} from "vue";
|
import {nextTick, onMounted, ref, shallowRef} from "vue";
|
||||||
import Loading from "../misc/Loading.vue";
|
import Loading from "../misc/Loading.vue";
|
||||||
import {newPyodideWorker} from "./pyodide-worker-api.ts";
|
import {newPyodideWorker} from "./pyodide-worker-api.ts";
|
||||||
import {mdiCircleOpacity, mdiClose, mdiContentSave, mdiFolderOpen, mdiPlay, mdiReload, mdiShare} from "@mdi/js";
|
import {
|
||||||
|
mdiBroom,
|
||||||
|
mdiCircleOpacity,
|
||||||
|
mdiClose,
|
||||||
|
mdiContentSave,
|
||||||
|
mdiFolderOpen,
|
||||||
|
mdiPlay,
|
||||||
|
mdiReload,
|
||||||
|
mdiShare
|
||||||
|
} from "@mdi/js";
|
||||||
import {VBtn, VCard, VCardText, VSlider, VSpacer, VToolbar, VToolbarTitle, VTooltip} from "vuetify/components";
|
import {VBtn, VCard, VCardText, VSlider, VSpacer, VToolbar, VToolbarTitle, VTooltip} from "vuetify/components";
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import SvgIcon from '@jamescoyle/vue-icon';
|
import SvgIcon from '@jamescoyle/vue-icon';
|
||||||
import {version as pyodideVersion} from "pyodide";
|
import {version as pyodideVersion} from "pyodide";
|
||||||
import {gzip} from 'pako';
|
import {gzip} from 'pako';
|
||||||
import {b66Encode} from "./b66.ts";
|
import {b64UrlEncode} from "./b64.ts";
|
||||||
import {Base64} from 'js-base64'; // More compatible with binary data from python...
|
import {Base64} from 'js-base64'; // More compatible with binary data from python...
|
||||||
import {NetworkUpdateEvent, NetworkUpdateEventModel} from "../misc/network.ts";
|
import {NetworkUpdateEvent, NetworkUpdateEventModel} from "../misc/network.ts";
|
||||||
import {settings} from "../misc/settings.ts";
|
import {settings} from "../misc/settings.ts";
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import playgroundStartupCode from './PlaygroundStartup.py?raw';
|
import playgroundStartupCode from './PlaygroundStartup.py?raw';
|
||||||
|
|
||||||
const props = defineProps<{ initialCode: string }>();
|
const model = defineModel<{ code: string, firstTime: boolean }>({required: true}); // Initial code should only be set on first load!
|
||||||
const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>()
|
const emit = defineEmits<{ close: [], updateModel: [NetworkUpdateEvent] }>()
|
||||||
|
|
||||||
// ============ LOAD MONACO EDITOR ============
|
// ============ LOAD MONACO EDITOR ============
|
||||||
setupMonaco() // Must be called before using the editor
|
setupMonaco() // Must be called before using the editor
|
||||||
|
|
||||||
const code = ref((import.meta as any)?.hot?.data?.code || props.initialCode);
|
|
||||||
const outputText = ref(``);
|
const outputText = ref(``);
|
||||||
|
|
||||||
function output(text: string) {
|
function output(text: string) {
|
||||||
@@ -51,21 +59,24 @@ const MONACO_EDITOR_OPTIONS = {
|
|||||||
const editorTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? `vs-dark` : `vs`
|
const editorTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? `vs-dark` : `vs`
|
||||||
const editor = shallowRef()
|
const editor = shallowRef()
|
||||||
const handleMount = (editorInstance: typeof VueMonacoEditor) => (editor.value = editorInstance)
|
const handleMount = (editorInstance: typeof VueMonacoEditor) => (editor.value = editorInstance)
|
||||||
const opacity = ref(0.9); // Opacity for the editor
|
const opacity = ref(0.9); // Opacity for the editor (overriden when settings are loaded)
|
||||||
|
|
||||||
// ============ LOAD PYODIDE (ASYNC) ============
|
// ============ LOAD PYODIDE (ASYNC) ============
|
||||||
let pyodideWorker: ReturnType<typeof newPyodideWorker> | null = (import.meta as any).hot?.data?.pyodideWorker || null;
|
let pyodideWorker: ReturnType<typeof newPyodideWorker> | null = (import.meta as any).hot?.data?.pyodideWorker || null;
|
||||||
const running = ref(true);
|
const running = ref(true);
|
||||||
|
|
||||||
async function setupPyodide() {
|
async function setupPyodide(first: boolean, loadSnapshot: Uint8Array | undefined = undefined) {
|
||||||
running.value = true;
|
running.value = true;
|
||||||
if (opacity.value == 0.0) opacity.value = 0.9; // User doesn't know how to show code again, reset after reopening
|
if (opacity.value == 0.0 && !first) opacity.value = 0.9; // User doesn't know how to show code again, reset after reopening
|
||||||
if (pyodideWorker === null) {
|
if (pyodideWorker === null) {
|
||||||
output("Creating new Pyodide worker...\n");
|
output("Creating new Pyodide worker...\n");
|
||||||
pyodideWorker = newPyodideWorker({
|
pyodideWorker = newPyodideWorker(Object.assign({
|
||||||
indexURL: `https://cdn.jsdelivr.net/pyodide/v${pyodideVersion}/full/`, // FIXME: Local deployment?
|
// Note: python wheels are downloaded from the CDN, as we can't know which ones are needed in advance to bundle them
|
||||||
|
// Furthermore, this lets us use the latest version of all wheels including ocp-specific ones without app updates
|
||||||
|
indexURL: `https://cdn.jsdelivr.net/pyodide/v${pyodideVersion}/full/`,
|
||||||
packages: ["micropip", "sqlite3"], // Faster load if done here
|
packages: ["micropip", "sqlite3"], // Faster load if done here
|
||||||
});
|
// _makeSnapshot: true, // Enable snapshotting for faster startup (still experimental: breaks loading any packages)
|
||||||
|
}, (loadSnapshot ? {_loadSnapshot: loadSnapshot} : {}))); // Load snapshot if provided
|
||||||
if ((import.meta as any).hot) (import.meta as any).hot.data.pyodideWorker = pyodideWorker
|
if ((import.meta as any).hot) (import.meta as any).hot.data.pyodideWorker = pyodideWorker
|
||||||
} else {
|
} else {
|
||||||
output("Reusing existing Pyodide instance...\n");
|
output("Reusing existing Pyodide instance...\n");
|
||||||
@@ -73,7 +84,7 @@ async function setupPyodide() {
|
|||||||
output("Preloading packages...\n");
|
output("Preloading packages...\n");
|
||||||
await pyodideWorker.asyncRun(playgroundStartupCode, output, output); // Also import yacv_server and mock ocp_vscode here for faster custom code execution
|
await pyodideWorker.asyncRun(playgroundStartupCode, output, output); // Also import yacv_server and mock ocp_vscode here for faster custom code execution
|
||||||
running.value = false; // Indicate that Pyodide is ready
|
running.value = false; // Indicate that Pyodide is ready
|
||||||
output("Pyodide worker initialized.\n");
|
output("Pyodide worker ready.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runCode() {
|
async function runCode() {
|
||||||
@@ -88,8 +99,7 @@ async function runCode() {
|
|||||||
output("Running code...\n");
|
output("Running code...\n");
|
||||||
try {
|
try {
|
||||||
running.value = true;
|
running.value = true;
|
||||||
if ((import.meta as any).hot) (import.meta as any).hot.data.code = code.value; // Save code for hot reload
|
await pyodideWorker.asyncRun(model.value.code, output, (msg: string) => {
|
||||||
await pyodideWorker.asyncRun(code.value, output, (msg: string) => {
|
|
||||||
// Detect models printed to console (since http server is not available in pyodide)
|
// Detect models printed to console (since http server is not available in pyodide)
|
||||||
if (msg.startsWith(yacvServerModelPrefix)) {
|
if (msg.startsWith(yacvServerModelPrefix)) {
|
||||||
const modelData = msg.slice(yacvServerModelPrefix.length);
|
const modelData = msg.slice(yacvServerModelPrefix.length);
|
||||||
@@ -99,7 +109,7 @@ async function runCode() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
output(`Error running initial code: ${e}\n`);
|
output(`Error running code: ${e}\n`);
|
||||||
} finally {
|
} finally {
|
||||||
running.value = false; // Indicate that Pyodide is ready
|
running.value = false; // Indicate that Pyodide is ready
|
||||||
}
|
}
|
||||||
@@ -131,7 +141,7 @@ function onModelData(modelData: string) {
|
|||||||
"Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString());
|
"Invalid GLTF binary data received: " + binaryData.slice(0, 4).toString());
|
||||||
// - Create a Blob from the binary data to be used as a URL
|
// - Create a Blob from the binary data to be used as a URL
|
||||||
const blob = new Blob([binaryData], {type: 'model/gltf-binary'});
|
const blob = new Blob([binaryData], {type: 'model/gltf-binary'});
|
||||||
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata
|
modelMetadata.url = URL.createObjectURL(blob); // Set the hacked URL in the model metadata XXX: revoked on App.vue
|
||||||
}
|
}
|
||||||
// - Emit the event with the model metadata and URL
|
// - Emit the event with the model metadata and URL
|
||||||
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {
|
let networkUpdateEvent = new NetworkUpdateEvent([modelMetadata], () => {
|
||||||
@@ -139,22 +149,26 @@ function onModelData(modelData: string) {
|
|||||||
emit('updateModel', networkUpdateEvent);
|
emit('updateModel', networkUpdateEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWorker() {
|
function resetWorker(loadSnapshot: Uint8Array | undefined = undefined) {
|
||||||
if (pyodideWorker) {
|
if (pyodideWorker) {
|
||||||
pyodideWorker.terminate(); // Terminate existing worker
|
pyodideWorker.terminate(); // Terminate existing worker
|
||||||
pyodideWorker = null; // Reset worker reference
|
pyodideWorker = null; // Reset worker reference
|
||||||
}
|
}
|
||||||
outputText.value = ``; // Clear output text
|
outputText.value = ``; // Clear output text
|
||||||
setupPyodide(); // Reinitialize Pyodide
|
setupPyodide(false, loadSnapshot); // Reinitialize Pyodide
|
||||||
}
|
}
|
||||||
|
|
||||||
function shareLink() {
|
function shareLink() {
|
||||||
const baseUrl = window.location
|
const baseUrl = window.location
|
||||||
const urlParams = new URLSearchParams(baseUrl.hash.slice(1)); // Keep all previous URL parameters
|
const searchParams = new URLSearchParams(baseUrl.search);
|
||||||
urlParams.set('pg_code', b66Encode(gzip(code.value, {level: 9}))); // Compress and encode the code
|
searchParams.delete('pg_code_url'); // Remove any existing pg_code parameter
|
||||||
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}${baseUrl.search}#${urlParams.toString()}`; // Prefer hash to GET (bigger limits)
|
searchParams.delete('pg_code'); // Remove any existing pg_code parameter
|
||||||
|
const hashParams = new URLSearchParams(baseUrl.hash.slice(1)); // Keep all previous URL parameters
|
||||||
|
hashParams.delete('pg_code_url') // Would overwrite the pg_code parameter
|
||||||
|
hashParams.set('pg_code', b64UrlEncode(gzip(model.value.code, {level: 9}))); // Compress and encode the code
|
||||||
|
const shareUrl = `${baseUrl.origin}${baseUrl.pathname}?${searchParams}#${hashParams}`; // Prefer hash to GET
|
||||||
output(`Share link ready: ${shareUrl}\n`)
|
output(`Share link ready: ${shareUrl}\n`)
|
||||||
if (!navigator.clipboard) {
|
if (navigator.clipboard?.writeText === undefined) {
|
||||||
output("Clipboard API not available. Please copy the link manually.\n");
|
output("Clipboard API not available. Please copy the link manually.\n");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -172,27 +186,28 @@ function loadSnapshot() {
|
|||||||
throw new Error("Not implemented yet!"); // TODO: Implement snapshot loading
|
throw new Error("Not implemented yet!"); // TODO: Implement snapshot loading
|
||||||
}
|
}
|
||||||
|
|
||||||
const reused = (import.meta as any).hot?.data?.pyodideWorker !== undefined;
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const sett = await settings()
|
const sett = await settings
|
||||||
if (!reused) opacity.value = sett.pg_opacity_loading
|
if (model.value.firstTime) opacity.value = sett.pg_opacity_loading
|
||||||
await setupPyodide()
|
await setupPyodide(true);
|
||||||
if (props.initialCode != "" && !reused) await runCode();
|
if (model.value.firstTime) {
|
||||||
if (!reused) opacity.value = sett.pg_opacity_loaded
|
await runCode();
|
||||||
|
opacity.value = sett.pg_opacity_loaded
|
||||||
|
model.value.firstTime = false
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Add keyboard shortcuts
|
// Add keyboard shortcuts
|
||||||
const editorRef = ref<HTMLElement | null>(null);
|
const editorRef = ref<HTMLElement | null>(null);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (editorRef.value) {
|
if (editorRef.value) {
|
||||||
console.log(editorRef.value)
|
|
||||||
editorRef.value.addEventListener('keydown', (event: Event) => {
|
editorRef.value.addEventListener('keydown', (event: Event) => {
|
||||||
if (!(event instanceof KeyboardEvent)) return; // Ensure event is a KeyboardEvent
|
if (!(event instanceof KeyboardEvent)) return; // Ensure event is a KeyboardEvent
|
||||||
if (event.key === 'Enter' && event.ctrlKey) {
|
if (event.key === 'F10') { // Run code on F10
|
||||||
event.preventDefault(); // Prevent default behavior of Enter key
|
event.preventDefault(); // Prevent default behavior of the key
|
||||||
runCode(); // Run code on Ctrl+Enter
|
runCode();
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') { // Close on Escape key
|
||||||
emit('close'); // Close on Escape key
|
emit('close');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -256,11 +271,17 @@ onMounted(() => {
|
|||||||
<!-- Only show content if opacity is greater than 0 -->
|
<!-- Only show content if opacity is greater than 0 -->
|
||||||
<div class="playground-container">
|
<div class="playground-container">
|
||||||
<div class="playground-editor" ref="editorRef">
|
<div class="playground-editor" ref="editorRef">
|
||||||
<VueMonacoEditor v-model:value="code" :theme="editorTheme" :options="MONACO_EDITOR_OPTIONS"
|
<VueMonacoEditor v-model:value="model.code" :theme="editorTheme" :options="MONACO_EDITOR_OPTIONS"
|
||||||
language="python" @mount="handleMount"/>
|
language="python" @mount="handleMount"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="playground-console">
|
<div class="playground-console">
|
||||||
<h3>Console Output</h3>
|
<h3 style="display:flex; align-items: center; justify-content: space-between; margin: 0;">
|
||||||
|
Console Output
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="outputText = ''">
|
||||||
|
<svg-icon :path="mdiBroom" type="mdi" class="h-"/>
|
||||||
|
</v-btn>
|
||||||
|
</h3>
|
||||||
<pre>{{ outputText }}</pre> <!-- Placeholder for console output -->
|
<pre>{{ outputText }}</pre> <!-- Placeholder for console output -->
|
||||||
<Loading v-if="running"/>
|
<Loading v-if="running"/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ await micropip.install("lib3mf")
|
|||||||
micropip.add_mock_package("py-lib3mf", "2.4.1", modules={"py_lib3mf": 'from lib3mf import *'})
|
micropip.add_mock_package("py-lib3mf", "2.4.1", modules={"py_lib3mf": 'from lib3mf import *'})
|
||||||
|
|
||||||
# Install the yacv_server package, which is the main server for the OCP.wasm playground; and also preinstalls build123d.
|
# Install the yacv_server package, which is the main server for the OCP.wasm playground; and also preinstalls build123d.
|
||||||
await micropip.install("yacv_server")
|
await micropip.install("yacv_server", pre=True)
|
||||||
|
|
||||||
# Preimport the yacv_server package to ensure it is available in the global scope, and mock the ocp_vscode package.
|
# Preimport the yacv_server package to ensure it is available in the global scope, and mock the ocp_vscode package.
|
||||||
from yacv_server import *
|
from yacv_server import *
|
||||||
@@ -16,23 +16,21 @@ micropip.add_mock_package("ocp-vscode", "2.8.9", modules={"ocp_vscode": 'from ya
|
|||||||
show_object = show
|
show_object = show
|
||||||
|
|
||||||
# Preinstall a font to avoid issues with no font being available.
|
# Preinstall a font to avoid issues with no font being available.
|
||||||
def install_font_to_ocp(font_url, font_name=None):
|
async def install_font_to_ocp(font_url, font_name=None):
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from pyodide.http import pyfetch
|
from pyodide.http import pyfetch
|
||||||
from OCP.Font import Font_FontMgr, Font_SystemFont, Font_FA_Regular
|
from OCP.Font import Font_FontMgr, Font_SystemFont, Font_FA_Regular
|
||||||
from OCP.TCollection import TCollection_AsciiString
|
from OCP.TCollection import TCollection_AsciiString
|
||||||
import os, asyncio
|
import os
|
||||||
|
|
||||||
|
# Prepare the font path and name
|
||||||
font_name = font_name if font_name is not None else font_url.split("/")[-1]
|
font_name = font_name if font_name is not None else font_url.split("/")[-1]
|
||||||
|
|
||||||
# Choose a "system-like" font directory
|
|
||||||
font_path = os.path.join("/tmp", font_name)
|
font_path = os.path.join("/tmp", font_name)
|
||||||
os.makedirs(os.path.dirname(font_path), exist_ok=True)
|
os.makedirs(os.path.dirname(font_path), exist_ok=True)
|
||||||
|
|
||||||
# Download the font using pyfetch
|
# Download the font using pyfetch
|
||||||
loop = asyncio.get_event_loop()
|
response = await pyfetch(font_url)
|
||||||
response = loop.run_until_complete(pyfetch(font_url))
|
font_data = await response.bytes()
|
||||||
font_data = loop.run_until_complete(response.bytes())
|
|
||||||
|
|
||||||
# Save it to the system-like folder
|
# Save it to the system-like folder
|
||||||
with open(font_path, "wb") as f:
|
with open(font_path, "wb") as f:
|
||||||
@@ -47,4 +45,4 @@ def install_font_to_ocp(font_url, font_name=None):
|
|||||||
|
|
||||||
|
|
||||||
# Make sure there is at least one font installed, so that the tests can run
|
# Make sure there is at least one font installed, so that the tests can run
|
||||||
install_font_to_ocp("https://raw.githubusercontent.com/xbmc/xbmc/d3a7f95f3f017b8e861d5d95cc4b33eef4286ce2/media/Fonts/arial.ttf")
|
await install_font_to_ocp("https://raw.githubusercontent.com/xbmc/xbmc/d3a7f95f3f017b8e861d5d95cc4b33eef4286ce2/media/Fonts/arial.ttf")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import SvgIcon from '@jamescoyle/vue-icon';
|
|||||||
import type {ModelViewerElement} from '@google/model-viewer';
|
import type {ModelViewerElement} from '@google/model-viewer';
|
||||||
import Loading from "../misc/Loading.vue";
|
import Loading from "../misc/Loading.vue";
|
||||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||||
import {defineAsyncComponent, ref, type Ref} from "vue";
|
import {defineAsyncComponent, ref} from "vue";
|
||||||
import type {SelectionInfo} from "./selection";
|
import type {SelectionInfo} from "./selection";
|
||||||
import {settings} from "../misc/settings.ts";
|
import {settings} from "../misc/settings.ts";
|
||||||
import type {NetworkUpdateEvent} from "../misc/network.ts";
|
import type {NetworkUpdateEvent} from "../misc/network.ts";
|
||||||
@@ -59,12 +59,14 @@ const emit = defineEmits<{ findModel: [string], updateModel: [NetworkUpdateEvent
|
|||||||
|
|
||||||
const sett = ref<any | null>(null);
|
const sett = ref<any | null>(null);
|
||||||
const showPlaygroundDialog = ref(false);
|
const showPlaygroundDialog = ref(false);
|
||||||
|
const pg_model = ref({code: '# Loading...', firstTime: false});
|
||||||
(async () => {
|
(async () => {
|
||||||
sett.value = await settings();
|
sett.value = await settings;
|
||||||
showPlaygroundDialog.value = sett.value.pg_code != "";
|
pg_model.value = {code: sett.value.pg_code, firstTime: true};
|
||||||
|
showPlaygroundDialog.value = pg_model.value.code != "";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
let selection: Ref<Array<SelectionInfo>> = ref([]);
|
let selection = ref<Array<SelectionInfo>>([]);
|
||||||
let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length
|
let selectionFaceCount = () => selection.value.filter((s) => s.kind == 'face').length
|
||||||
let selectionEdgeCount = () => selection.value.filter((s) => s.kind == 'edge').length
|
let selectionEdgeCount = () => selection.value.filter((s) => s.kind == 'edge').length
|
||||||
let selectionVertexCount = () => selection.value.filter((s) => s.kind == "vertex").length
|
let selectionVertexCount = () => selection.value.filter((s) => s.kind == "vertex").length
|
||||||
@@ -126,6 +128,7 @@ async function downloadSceneGlb() {
|
|||||||
link.download = file.name;
|
link.download = file.name;
|
||||||
link.href = URL.createObjectURL(file);
|
link.href = URL.createObjectURL(file);
|
||||||
link.click();
|
link.click();
|
||||||
|
URL.revokeObjectURL(link.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openGithub() {
|
async function openGithub() {
|
||||||
@@ -193,7 +196,7 @@ document.addEventListener('keydown', (event) => {
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:default="{ isActive }">
|
<template v-slot:default="{ isActive }">
|
||||||
<if-not-small-build>
|
<if-not-small-build>
|
||||||
<playground-dialog-content v-if="sett != null" :initial-code="sett.pg_code" @close="isActive.value = false"
|
<playground-dialog-content v-if="sett != null" v-model="pg_model" @close="isActive.value = false"
|
||||||
@update-model="(event: NetworkUpdateEvent) => emit('updateModel', event)"/>
|
@update-model="(event: NetworkUpdateEvent) => emit('updateModel', event)"/>
|
||||||
</if-not-small-build>
|
</if-not-small-build>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
21
frontend/tools/b64.ts
Normal file
21
frontend/tools/b64.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export function b64UrlEncode(data: Uint8Array): string {
|
||||||
|
const base64 = btoa(String.fromCharCode(...data));
|
||||||
|
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function b64UrlDecode(encoded: string): Uint8Array {
|
||||||
|
// Replace URL-safe characters with standard base64 characters
|
||||||
|
let base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
// Add padding if necessary
|
||||||
|
const padding = base64.length % 4;
|
||||||
|
if (padding) {
|
||||||
|
base64 += '='.repeat(4 - padding);
|
||||||
|
}
|
||||||
|
// Decode the base64 string to a byte array
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
const byteArray = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
byteArray[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return byteArray;
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// B66 encoding and decoding functions for compact url query parameter values. https://gist.github.com/danneu/6755394
|
|
||||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-_~";
|
|
||||||
|
|
||||||
export function b66Encode(data: Uint8Array): string {
|
|
||||||
let result = "";
|
|
||||||
let bits = 0;
|
|
||||||
let value = 0;
|
|
||||||
|
|
||||||
for (let byte of data) {
|
|
||||||
value = (value << 8) | byte;
|
|
||||||
bits += 8;
|
|
||||||
|
|
||||||
while (bits >= 6) {
|
|
||||||
bits -= 6;
|
|
||||||
result += alphabet[(value >> bits) & 0x3F];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bits > 0) {
|
|
||||||
result += alphabet[(value << (6 - bits)) & 0x3F];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function b66Decode(encoded: string): Uint8Array {
|
|
||||||
let result = [];
|
|
||||||
let bits = 0;
|
|
||||||
let value = 0;
|
|
||||||
|
|
||||||
for (let char of encoded) {
|
|
||||||
const index = alphabet.indexOf(char);
|
|
||||||
if (index === -1) {
|
|
||||||
throw new Error(`Invalid character '${char}' in B66 encoded string.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
value = (value << 6) | index;
|
|
||||||
bits += 6;
|
|
||||||
|
|
||||||
while (bits >= 8) {
|
|
||||||
bits -= 8;
|
|
||||||
result.push((value >> bits) & 0xFF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bits > 0) {
|
|
||||||
// If there are leftover bits, they should not be present in a valid B66 encoding.
|
|
||||||
if (value << (8 - bits)) {
|
|
||||||
throw new Error("Invalid B66 encoding: leftover bits.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Uint8Array(result);
|
|
||||||
}
|
|
||||||
@@ -38,6 +38,7 @@ export function newPyodideWorker(initOpts: Parameters<typeof loadPyodide>[0]) {
|
|||||||
mkdirTree: (path: string) => commonRequestResponse({type: "mkdirTree", id: requestId++, path}),
|
mkdirTree: (path: string) => commonRequestResponse({type: "mkdirTree", id: requestId++, path}),
|
||||||
writeFile: (path: string, content: string) =>
|
writeFile: (path: string, content: string) =>
|
||||||
commonRequestResponse({type: "writeFile", id: requestId++, path, content}),
|
commonRequestResponse({type: "writeFile", id: requestId++, path, content}),
|
||||||
|
makeSnapshot: () => commonRequestResponse({type: "makeSnapshot", id: requestId++}),
|
||||||
terminate: () => worker.terminate()
|
terminate: () => worker.terminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export type MessageEventDataIn = {
|
|||||||
id: number;
|
id: number;
|
||||||
path: string;
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
} | {
|
||||||
|
type: 'makeSnapshot';
|
||||||
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
|
self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
|
||||||
@@ -64,6 +67,15 @@ self.onmessage = async (event: MessageEvent<MessageEventDataIn>) => {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
self.postMessage({id: event.data.id, error: error.message});
|
self.postMessage({id: event.data.id, error: error.message});
|
||||||
}
|
}
|
||||||
|
} else if (event.data.type === 'makeSnapshot') {
|
||||||
|
// Take a snapshot of the current Pyodide filesystem.
|
||||||
|
const pyodide = await pyodideReadyPromise;
|
||||||
|
try {
|
||||||
|
const snapshot = pyodide.makeMemorySnapshot();
|
||||||
|
self.postMessage({id: event.data.id, result: snapshot});
|
||||||
|
} catch (error: any) {
|
||||||
|
self.postMessage({id: event.data.id, error: error.message});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Unknown message type:", (event.data as any)?.type);
|
console.error("Unknown message type:", (event.data as any)?.type);
|
||||||
self.postMessage({id: (event.data as any)?.id, error: "Unknown message type: " + (event.data as any)?.type});
|
self.postMessage({id: (event.data as any)?.id, error: "Unknown message type: " + (event.data as any)?.type});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const renderer = ref<Renderer | null>(null);
|
|||||||
const controls = ref<SmoothControls | null>(null);
|
const controls = ref<SmoothControls | null>(null);
|
||||||
|
|
||||||
const sett = ref<any | null>(null);
|
const sett = ref<any | null>(null);
|
||||||
(async () => sett.value = await settings())();
|
(async () => sett.value = await settings)();
|
||||||
|
|
||||||
let lastCameraTargetPosition: Vector3 | undefined = undefined;
|
let lastCameraTargetPosition: Vector3 | undefined = undefined;
|
||||||
let lastCameraZoom: number | undefined = undefined;
|
let lastCameraZoom: number | undefined = undefined;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {settings} from "../misc/settings.ts";
|
|||||||
export let currentSceneRotation = 0; // radians, 0 is the default rotation
|
export let currentSceneRotation = 0; // radians, 0 is the default rotation
|
||||||
|
|
||||||
export async function setupLighting(modelViewer: ModelViewerElement) {
|
export async function setupLighting(modelViewer: ModelViewerElement) {
|
||||||
modelViewer[$scene].environmentIntensity = (await settings()).environmentIntensity;
|
modelViewer[$scene].environmentIntensity = (await settings).environmentIntensity;
|
||||||
// Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
|
// Code is mostly copied from the example at: https://modelviewer.dev/examples/stagingandcameras/#turnSkybox
|
||||||
let lastX: number;
|
let lastX: number;
|
||||||
let panning = false;
|
let panning = false;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yet-another-cad-viewer",
|
"name": "yet-another-cad-viewer",
|
||||||
"version": "0.10.0-rc.1",
|
"version": "0.10.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "yacv-server"
|
name = "yacv-server"
|
||||||
version = "0.10.0-rc.1"
|
version = "0.10.0"
|
||||||
description = "Yet Another CAD Viewer (server)"
|
description = "Yet Another CAD Viewer (server)"
|
||||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ class YACV:
|
|||||||
"""Initializes the YACV server"""
|
"""Initializes the YACV server"""
|
||||||
raw_protocol = os.getenv('YACV_PROTOCOL', 'http' if sys.platform != 'emscripten' else 'stderr').upper()
|
raw_protocol = os.getenv('YACV_PROTOCOL', 'http' if sys.platform != 'emscripten' else 'stderr').upper()
|
||||||
self.protocol = YACVProtocol[raw_protocol] if raw_protocol in YACVProtocol.__members__ else YACVProtocol.HTTP
|
self.protocol = YACVProtocol[raw_protocol] if raw_protocol in YACVProtocol.__members__ else YACVProtocol.HTTP
|
||||||
self.protocol = YACVProtocol.STDERR
|
|
||||||
self.server_thread = None
|
self.server_thread = None
|
||||||
self.server = None
|
self.server = None
|
||||||
self.startup_complete = threading.Event()
|
self.startup_complete = threading.Event()
|
||||||
|
|||||||
@@ -2940,6 +2940,7 @@ three-mesh-bvh@^0.9.0:
|
|||||||
|
|
||||||
"three-orientation-gizmo@git+https://github.com/jrj2211/three-orientation-gizmo.git":
|
"three-orientation-gizmo@git+https://github.com/jrj2211/three-orientation-gizmo.git":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
|
uid "000281f0559c316f72cdd23a1885d63ae6901095"
|
||||||
resolved "git+https://github.com/jrj2211/three-orientation-gizmo.git#000281f0559c316f72cdd23a1885d63ae6901095"
|
resolved "git+https://github.com/jrj2211/three-orientation-gizmo.git#000281f0559c316f72cdd23a1885d63ae6901095"
|
||||||
dependencies:
|
dependencies:
|
||||||
three "^0.125.0"
|
three "^0.125.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user