Basic URL loading support
This commit is contained in:
parent
ecdc04af39
commit
dd3d385899
9 changed files with 109 additions and 59 deletions
|
|
@ -107,4 +107,4 @@ export function App({initialState, statePersister, fs}: {initialState: State, st
|
|||
</FSContext.Provider>
|
||||
</ModelContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Button } from 'primereact/button';
|
|||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { Menu } from 'primereact/menu';
|
||||
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 FilePicker, { } from './FilePicker';
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ export default function EditorPanel({className, style}: {className?: string, sty
|
|||
{
|
||||
label: "New project",
|
||||
icon: 'pi pi-plus-circle',
|
||||
url: buildUrlForStateParams(blankProjectState),
|
||||
command: async () => window.open(buildUrlForStateParams(await getBlankProjectState()), '_blank'),
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ window.addEventListener('load', async () => {
|
|||
};
|
||||
}
|
||||
|
||||
const initialState = createInitialState(persistedState);
|
||||
const initialState = await createInitialState(persistedState);
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { createEditorFS, getParentDir, symlinkLibraries } from "../fs/filesystem
|
|||
import { OpenSCADInvocation, OpenSCADInvocationCallback, OpenSCADInvocationResults } from "./openscad-runner";
|
||||
import { deployedArchiveNames, zipArchives } from "../fs/zip-archives";
|
||||
import { fetchSource } from "../utils.js";
|
||||
import { stderr } from "process";
|
||||
declare var BrowserFS: BrowserFSInterface
|
||||
|
||||
importScripts("browserfs.min.js");
|
||||
|
|
@ -94,7 +93,13 @@ addEventListener('message', async (e) => {
|
|||
for (const source of inputs) {
|
||||
try {
|
||||
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) {
|
||||
console.trace(e);
|
||||
throw new Error(`Error while trying to write ${source.path}: ${e}`);
|
||||
|
|
|
|||
|
|
@ -52,15 +52,19 @@ export async function readStateFromFragment(): Promise<State | null> {
|
|||
try {
|
||||
const serialized = window.location.hash.substring(1);
|
||||
if (serialized === 'blank') {
|
||||
return createInitialState(null, '');
|
||||
} else if (serialized.startsWith('src:')) {
|
||||
return createInitialState(null, {content: ''});
|
||||
} else if (serialized.startsWith('src=')) {
|
||||
// For testing
|
||||
const src = decodeURIComponent(serialized.substring('src:'.length));
|
||||
return createInitialState(null, src);
|
||||
} else if (serialized.startsWith('testpath:')) {
|
||||
const src = decodeURIComponent(serialized.substring('src='.length));
|
||||
return createInitialState(null, {content: src});
|
||||
} else if (serialized.startsWith('path=')) {
|
||||
const path = decodeURIComponent(serialized.substring('path='.length));
|
||||
return createInitialState(null, {path});
|
||||
} else if (serialized.startsWith('url=')) {
|
||||
// For testing
|
||||
const path = decodeURIComponent(serialized.substring('testpath:'.length));
|
||||
return createInitialState(null, '...', path);
|
||||
const url = decodeURIComponent(serialized.substring('url='.length));
|
||||
const path = '/' + new URL(url).pathname.split('/').pop();
|
||||
return createInitialState(null, {path, url});
|
||||
}
|
||||
let obj;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,36 +2,52 @@
|
|||
|
||||
import defaultScad from './default-scad';
|
||||
import { State } from './app-state';
|
||||
import { fetchSource } from '../utils';
|
||||
|
||||
export const defaultSourcePath = '/playground.scad';
|
||||
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'];
|
||||
const mode: Mode = window.matchMedia("(min-width: 768px)").matches
|
||||
? 'multi' : 'single';
|
||||
|
||||
const initialState: State = {
|
||||
params: {
|
||||
activePath: activePath,
|
||||
sources: [{path: activePath, content}],
|
||||
features: [],
|
||||
exportFormat2D: 'svg',
|
||||
exportFormat3D: 'glb',
|
||||
},
|
||||
view: {
|
||||
layout: {
|
||||
mode: 'multi',
|
||||
editor: true,
|
||||
viewer: true,
|
||||
customizer: false,
|
||||
} as any,
|
||||
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: {
|
||||
activePath,
|
||||
sources: [{path: activePath, content, url}],
|
||||
features: [],
|
||||
exportFormat2D: 'svg',
|
||||
exportFormat3D: 'glb',
|
||||
},
|
||||
view: {
|
||||
layout: {
|
||||
mode: 'multi',
|
||||
editor: true,
|
||||
viewer: true,
|
||||
customizer: false,
|
||||
} as any,
|
||||
|
||||
color: defaultModelColor,
|
||||
},
|
||||
...(state ?? {})
|
||||
};
|
||||
color: defaultModelColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (initialState.view.layout.mode != mode) {
|
||||
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);
|
||||
// if (initialState.params.sourcePath !== defaultSourcePath) {
|
||||
|
|
@ -67,5 +83,9 @@ export function createInitialState(state: State | null, content = defaultScad, a
|
|||
return initialState;
|
||||
}
|
||||
|
||||
|
||||
export const blankProjectState: State = createInitialState(null, '');
|
||||
export async function getBlankProjectState() {
|
||||
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 chroma from "chroma-js";
|
||||
|
||||
const githubRx = /^https:\/\/github.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/;
|
||||
|
||||
export class Model {
|
||||
constructor(private fs: FS, public state: State, private setStateCallback?: (state: State) => void,
|
||||
private statePersister?: StatePersister) {
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.state.output && !this.state.lastCheckerRun && !this.state.previewing && !this.state.checkingSyntax && !this.state.rendering &&
|
||||
this.source.trim() != '') {
|
||||
if (!this.state.output && !this.state.lastCheckerRun && !this.state.previewing && !this.state.checkingSyntax && !this.state.rendering) {
|
||||
this.processSource();
|
||||
}
|
||||
}
|
||||
|
|
@ -156,16 +157,26 @@ export class Model {
|
|||
}
|
||||
}
|
||||
|
||||
private processSource() {
|
||||
// const params = this.state.params;
|
||||
// if (isFileWritable(params.sourcePath)) {
|
||||
// const absolutePath = params.sourcePath.startsWith('/') ? params.sourcePath : `/${params.sourcePath}`;
|
||||
// this.fs.writeFile(params.sourcePath, params.source);
|
||||
// }
|
||||
if (this.state.params.activePath.endsWith('.scad')) {
|
||||
this.checkSyntax();
|
||||
private async processSource() {
|
||||
let src = this.state.params.sources.find(src => src.path === this.state.params.activePath);
|
||||
if (src && src.content == null) {
|
||||
let {path, url} = src;
|
||||
// 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')) {
|
||||
this.checkSyntax();
|
||||
}
|
||||
this.render({isPreview: true, now: false});
|
||||
}
|
||||
this.render({isPreview: true, now: false});
|
||||
}
|
||||
|
||||
async checkSyntax() {
|
||||
|
|
@ -304,7 +315,7 @@ export class Model {
|
|||
if (path.startsWith('/')) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
zip.file(path, await fetchSource(source));
|
||||
zip.file(path, await fetchSource(this.fs, source));
|
||||
}
|
||||
zip.generateAsync({type: 'blob'}).then(blob => {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
return content;
|
||||
return new TextEncoder().encode(content);
|
||||
} else if (url) {
|
||||
if (path.endsWith('.scad') || path.endsWith('.json')) {
|
||||
if (isText) {
|
||||
content = await (await fetch(url)).text();
|
||||
return content.replace(/\r\n/g, '\n');
|
||||
return new TextEncoder().encode(content.replace(/\r\n/g, '\n'));
|
||||
} else {
|
||||
// Fetch bytes
|
||||
const response = await fetch(url);
|
||||
|
|
@ -165,6 +166,9 @@ export async function fetchSource({content, path, url}: Source) {
|
|||
const data = new Uint8Array(buffer);
|
||||
return data;
|
||||
}
|
||||
} else if (path) {
|
||||
const data = fs.readFileSync(path);
|
||||
return new Uint8Array('buffer' in data ? data.buffer : data);
|
||||
} else {
|
||||
throw new Error('Invalid source: ' + JSON.stringify({path, content, url}));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue