Basic URL loading support
This commit is contained in:
parent
ecdc04af39
commit
dd3d385899
9 changed files with 109 additions and 59 deletions
|
|
@ -9,7 +9,7 @@ import { Button } from 'primereact/button';
|
||||||
import { MenuItem } from 'primereact/menuitem';
|
import { MenuItem } from 'primereact/menuitem';
|
||||||
import { Menu } from 'primereact/menu';
|
import { Menu } from 'primereact/menu';
|
||||||
import { buildUrlForStateParams } from '../state/fragment-state';
|
import { buildUrlForStateParams } from '../state/fragment-state';
|
||||||
import { blankProjectState, defaultSourcePath } from '../state/initial-state';
|
import { getBlankProjectState, defaultSourcePath } from '../state/initial-state';
|
||||||
import { ModelContext, FSContext } from './contexts';
|
import { ModelContext, FSContext } from './contexts';
|
||||||
import FilePicker, { } from './FilePicker';
|
import FilePicker, { } from './FilePicker';
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ export default function EditorPanel({className, style}: {className?: string, sty
|
||||||
{
|
{
|
||||||
label: "New project",
|
label: "New project",
|
||||||
icon: 'pi pi-plus-circle',
|
icon: 'pi pi-plus-circle',
|
||||||
url: buildUrlForStateParams(blankProjectState),
|
command: async () => window.open(buildUrlForStateParams(await getBlankProjectState()), '_blank'),
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ window.addEventListener('load', async () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = createInitialState(persistedState);
|
const initialState = await createInitialState(persistedState);
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { createEditorFS, getParentDir, symlinkLibraries } from "../fs/filesystem
|
||||||
import { OpenSCADInvocation, OpenSCADInvocationCallback, OpenSCADInvocationResults } from "./openscad-runner";
|
import { OpenSCADInvocation, OpenSCADInvocationCallback, OpenSCADInvocationResults } from "./openscad-runner";
|
||||||
import { deployedArchiveNames, zipArchives } from "../fs/zip-archives";
|
import { deployedArchiveNames, zipArchives } from "../fs/zip-archives";
|
||||||
import { fetchSource } from "../utils.js";
|
import { fetchSource } from "../utils.js";
|
||||||
import { stderr } from "process";
|
|
||||||
declare var BrowserFS: BrowserFSInterface
|
declare var BrowserFS: BrowserFSInterface
|
||||||
|
|
||||||
importScripts("browserfs.min.js");
|
importScripts("browserfs.min.js");
|
||||||
|
|
@ -94,7 +93,13 @@ addEventListener('message', async (e) => {
|
||||||
for (const source of inputs) {
|
for (const source of inputs) {
|
||||||
try {
|
try {
|
||||||
console.log(`Writing ${source.path}`);
|
console.log(`Writing ${source.path}`);
|
||||||
instance.FS.writeFile(source.path, await fetchSource(source));
|
if (source.content == null && source.path != null && source.url == null) {
|
||||||
|
if (!instance.FS.isFile(source.path)) {
|
||||||
|
console.error(`File ${source.path} does not exist!`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
instance.FS.writeFile(source.path, await fetchSource(instance.FS, source));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.trace(e);
|
console.trace(e);
|
||||||
throw new Error(`Error while trying to write ${source.path}: ${e}`);
|
throw new Error(`Error while trying to write ${source.path}: ${e}`);
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,19 @@ export async function readStateFromFragment(): Promise<State | null> {
|
||||||
try {
|
try {
|
||||||
const serialized = window.location.hash.substring(1);
|
const serialized = window.location.hash.substring(1);
|
||||||
if (serialized === 'blank') {
|
if (serialized === 'blank') {
|
||||||
return createInitialState(null, '');
|
return createInitialState(null, {content: ''});
|
||||||
} else if (serialized.startsWith('src:')) {
|
} else if (serialized.startsWith('src=')) {
|
||||||
// For testing
|
// For testing
|
||||||
const src = decodeURIComponent(serialized.substring('src:'.length));
|
const src = decodeURIComponent(serialized.substring('src='.length));
|
||||||
return createInitialState(null, src);
|
return createInitialState(null, {content: src});
|
||||||
} else if (serialized.startsWith('testpath:')) {
|
} else if (serialized.startsWith('path=')) {
|
||||||
|
const path = decodeURIComponent(serialized.substring('path='.length));
|
||||||
|
return createInitialState(null, {path});
|
||||||
|
} else if (serialized.startsWith('url=')) {
|
||||||
// For testing
|
// For testing
|
||||||
const path = decodeURIComponent(serialized.substring('testpath:'.length));
|
const url = decodeURIComponent(serialized.substring('url='.length));
|
||||||
return createInitialState(null, '...', path);
|
const path = '/' + new URL(url).pathname.split('/').pop();
|
||||||
|
return createInitialState(null, {path, url});
|
||||||
}
|
}
|
||||||
let obj;
|
let obj;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,36 @@
|
||||||
|
|
||||||
import defaultScad from './default-scad';
|
import defaultScad from './default-scad';
|
||||||
import { State } from './app-state';
|
import { State } from './app-state';
|
||||||
|
import { fetchSource } from '../utils';
|
||||||
|
|
||||||
export const defaultSourcePath = '/playground.scad';
|
export const defaultSourcePath = '/playground.scad';
|
||||||
export const defaultModelColor = '#f9d72c';
|
export const defaultModelColor = '#f9d72c';
|
||||||
|
|
||||||
export function createInitialState(state: State | null, content = defaultScad, activePath = defaultSourcePath): State {
|
export async function createInitialState(state: State | null, source?: {content?: string, path?: string, url?: string}): Promise<State> {
|
||||||
|
|
||||||
type Mode = State['view']['layout']['mode'];
|
type Mode = State['view']['layout']['mode'];
|
||||||
const mode: Mode = window.matchMedia("(min-width: 768px)").matches
|
const mode: Mode = window.matchMedia("(min-width: 768px)").matches
|
||||||
? 'multi' : 'single';
|
? 'multi' : 'single';
|
||||||
|
|
||||||
const initialState: State = {
|
let initialState: State;
|
||||||
|
if (state) {
|
||||||
|
if (source) throw new Error('Cannot provide source when state is provided');
|
||||||
|
initialState = state;
|
||||||
|
} else {
|
||||||
|
let content, path, url;
|
||||||
|
if (source) {
|
||||||
|
content = source.content;
|
||||||
|
path = source.path;
|
||||||
|
url = source.url;
|
||||||
|
} else {
|
||||||
|
content = defaultScad;
|
||||||
|
path = defaultSourcePath;
|
||||||
|
}
|
||||||
|
let activePath = path ?? (url && new URL(url).pathname.split('/').pop()) ?? defaultSourcePath;
|
||||||
|
initialState = {
|
||||||
params: {
|
params: {
|
||||||
activePath: activePath,
|
activePath,
|
||||||
sources: [{path: activePath, content}],
|
sources: [{path: activePath, content, url}],
|
||||||
features: [],
|
features: [],
|
||||||
exportFormat2D: 'svg',
|
exportFormat2D: 'svg',
|
||||||
exportFormat3D: 'glb',
|
exportFormat3D: 'glb',
|
||||||
|
|
@ -30,8 +46,8 @@ export function createInitialState(state: State | null, content = defaultScad, a
|
||||||
|
|
||||||
color: defaultModelColor,
|
color: defaultModelColor,
|
||||||
},
|
},
|
||||||
...(state ?? {})
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (initialState.view.layout.mode != mode) {
|
if (initialState.view.layout.mode != mode) {
|
||||||
if (mode === 'multi' && initialState.view.layout.mode === 'single') {
|
if (mode === 'multi' && initialState.view.layout.mode === 'single') {
|
||||||
|
|
@ -51,7 +67,7 @@ export function createInitialState(state: State | null, content = defaultScad, a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialState.view.showAxes ??= true
|
initialState.view.showAxes ??= true;
|
||||||
|
|
||||||
// fs.writeFile(initialState.params.sourcePath, initialState.params.source);
|
// fs.writeFile(initialState.params.sourcePath, initialState.params.source);
|
||||||
// if (initialState.params.sourcePath !== defaultSourcePath) {
|
// if (initialState.params.sourcePath !== defaultSourcePath) {
|
||||||
|
|
@ -67,5 +83,9 @@ export function createInitialState(state: State | null, content = defaultScad, a
|
||||||
return initialState;
|
return initialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getBlankProjectState() {
|
||||||
export const blankProjectState: State = createInitialState(null, '');
|
return await createInitialState(null, {
|
||||||
|
path: defaultSourcePath,
|
||||||
|
content: defaultScad,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,15 @@ import { exportGlb } from "../io/export_glb";
|
||||||
import { export3MF } from "../io/export_3mf";
|
import { export3MF } from "../io/export_3mf";
|
||||||
import chroma from "chroma-js";
|
import chroma from "chroma-js";
|
||||||
|
|
||||||
|
const githubRx = /^https:\/\/github.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/;
|
||||||
|
|
||||||
export class Model {
|
export class Model {
|
||||||
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
|
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
|
||||||
private statePersister?: StatePersister) {
|
private statePersister?: StatePersister) {
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (!this.state.output && !this.state.lastCheckerRun && !this.state.previewing && !this.state.checkingSyntax && !this.state.rendering &&
|
if (!this.state.output && !this.state.lastCheckerRun && !this.state.previewing && !this.state.checkingSyntax && !this.state.rendering) {
|
||||||
this.source.trim() != '') {
|
|
||||||
this.processSource();
|
this.processSource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,17 +157,27 @@ export class Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processSource() {
|
private async processSource() {
|
||||||
// const params = this.state.params;
|
let src = this.state.params.sources.find(src => src.path === this.state.params.activePath);
|
||||||
// if (isFileWritable(params.sourcePath)) {
|
if (src && src.content == null) {
|
||||||
// const absolutePath = params.sourcePath.startsWith('/') ? params.sourcePath : `/${params.sourcePath}`;
|
let {path, url} = src;
|
||||||
// this.fs.writeFile(params.sourcePath, params.source);
|
// Transform https://github.com/tenstad/keyboard/blob/main/keyboard.scad to https://raw.githubusercontent.com/tenstad/keyboard/refs/heads/main/keyboard.scad
|
||||||
// }
|
let match;
|
||||||
|
if (url && (match = url.match(githubRx))) {
|
||||||
|
url = `https://raw.githubusercontent.com/${match[1]}/${match[2]}/refs/heads/${match[3]}`;
|
||||||
|
}
|
||||||
|
const content = new TextDecoder().decode(await fetchSource(this.fs, {path, url}));
|
||||||
|
this.mutate(s => {
|
||||||
|
s.params.sources = s.params.sources.map(src => src.path === s.params.activePath ? {...src, content} : src);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.source.trim() !== '') {
|
||||||
if (this.state.params.activePath.endsWith('.scad')) {
|
if (this.state.params.activePath.endsWith('.scad')) {
|
||||||
this.checkSyntax();
|
this.checkSyntax();
|
||||||
}
|
}
|
||||||
this.render({isPreview: true, now: false});
|
this.render({isPreview: true, now: false});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async checkSyntax() {
|
async checkSyntax() {
|
||||||
this.mutate(s => s.checkingSyntax = true);
|
this.mutate(s => s.checkingSyntax = true);
|
||||||
|
|
@ -304,7 +315,7 @@ export class Model {
|
||||||
if (path.startsWith('/')) {
|
if (path.startsWith('/')) {
|
||||||
path = path.substring(1);
|
path = path.substring(1);
|
||||||
}
|
}
|
||||||
zip.file(path, await fetchSource(source));
|
zip.file(path, await fetchSource(this.fs, source));
|
||||||
}
|
}
|
||||||
zip.generateAsync({type: 'blob'}).then(blob => {
|
zip.generateAsync({type: 'blob'}).then(blob => {
|
||||||
const file = new File([blob], 'project.zip');
|
const file = new File([blob], 'project.zip');
|
||||||
|
|
|
||||||
12
src/utils.ts
12
src/utils.ts
|
|
@ -151,13 +151,14 @@ export function downloadUrl(url: string, filename: string) {
|
||||||
link.parentNode?.removeChild(link);
|
link.parentNode?.removeChild(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSource({content, path, url}: Source) {
|
export async function fetchSource(fs: FS, {content, path, url}: Source): Promise<Uint8Array> {
|
||||||
|
const isText = path.endsWith('.scad') || path.endsWith('.json');
|
||||||
if (content) {
|
if (content) {
|
||||||
return content;
|
return new TextEncoder().encode(content);
|
||||||
} else if (url) {
|
} else if (url) {
|
||||||
if (path.endsWith('.scad') || path.endsWith('.json')) {
|
if (isText) {
|
||||||
content = await (await fetch(url)).text();
|
content = await (await fetch(url)).text();
|
||||||
return content.replace(/\r\n/g, '\n');
|
return new TextEncoder().encode(content.replace(/\r\n/g, '\n'));
|
||||||
} else {
|
} else {
|
||||||
// Fetch bytes
|
// Fetch bytes
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
@ -165,6 +166,9 @@ export async function fetchSource({content, path, url}: Source) {
|
||||||
const data = new Uint8Array(buffer);
|
const data = new Uint8Array(buffer);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
} else if (path) {
|
||||||
|
const data = fs.readFileSync(path);
|
||||||
|
return new Uint8Array('buffer' in data ? data.buffer : data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid source: ' + JSON.stringify({path, content, url}));
|
throw new Error('Invalid source: ' + JSON.stringify({path, content, url}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const longTimeout = 60000;
|
const longTimeout = 60000;
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
const url = isProd ? 'http://localhost:3000/dist/' : 'http://localhost:4000/';
|
const baseUrl = isProd ? 'http://localhost:3000/dist/' : 'http://localhost:4000/';
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|
||||||
|
|
@ -33,10 +33,13 @@ afterEach(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadSrc(src) {
|
function loadSrc(src) {
|
||||||
return page.goto(url + '#src:' + encodeURIComponent(src));
|
return page.goto(baseUrl + '#src=' + encodeURIComponent(src));
|
||||||
}
|
}
|
||||||
function loadPath(path) {
|
function loadPath(path) {
|
||||||
return page.goto(url + '#testpath:' + encodeURIComponent(path));
|
return page.goto(baseUrl + '#path=' + encodeURIComponent(path));
|
||||||
|
}
|
||||||
|
function loadUrl(url) {
|
||||||
|
return page.goto(baseUrl + '#url=' + encodeURIComponent(url));
|
||||||
}
|
}
|
||||||
async function waitForViewer() {
|
async function waitForViewer() {
|
||||||
await page.waitForSelector('model-viewer');
|
await page.waitForSelector('model-viewer');
|
||||||
|
|
@ -77,7 +80,7 @@ function waitForLabel(text) {
|
||||||
|
|
||||||
describe('e2e', () => {
|
describe('e2e', () => {
|
||||||
test('load the default page', async () => {
|
test('load the default page', async () => {
|
||||||
await page.goto(url);
|
await page.goto(baseUrl);
|
||||||
await waitForViewer();
|
await waitForViewer();
|
||||||
expectObjectList();
|
expectObjectList();
|
||||||
}, longTimeout);
|
}, longTimeout);
|
||||||
|
|
@ -112,6 +115,12 @@ describe('e2e', () => {
|
||||||
expect3DPolySet();
|
expect3DPolySet();
|
||||||
}, longTimeout);
|
}, longTimeout);
|
||||||
|
|
||||||
|
test('load a demo by url', async () => {
|
||||||
|
await loadUrl('https://github.com/tenstad/keyboard/blob/main/keyboard.scad');
|
||||||
|
await waitForViewer();
|
||||||
|
expect3DManifold();
|
||||||
|
}, longTimeout);
|
||||||
|
|
||||||
test('customizer & windows line endings work', async () => {
|
test('customizer & windows line endings work', async () => {
|
||||||
await loadSrc([
|
await loadSrc([
|
||||||
'myVar = 10;',
|
'myVar = 10;',
|
||||||
|
|
@ -119,11 +128,8 @@ describe('e2e', () => {
|
||||||
].join('\r\n'));
|
].join('\r\n'));
|
||||||
await waitForViewer();
|
await waitForViewer();
|
||||||
expect3DPolySet();
|
expect3DPolySet();
|
||||||
|
|
||||||
await (await waitForCustomizeButton()).click();
|
await (await waitForCustomizeButton()).click();
|
||||||
|
|
||||||
await page.waitForSelector('fieldset');
|
await page.waitForSelector('fieldset');
|
||||||
|
|
||||||
await waitForLabel('myVar');
|
await waitForLabel('myVar');
|
||||||
}, longTimeout);
|
}, longTimeout);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue