Skip to content

Commit

Permalink
Basic URL loading support
Browse files Browse the repository at this point in the history
  • Loading branch information
ochafik committed Dec 26, 2024
1 parent c55dca0 commit cc4cd1a
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,4 @@ export function App({initialState, statePersister, fs}: {initialState: State, st
</FSContext.Provider>
</ModelContext.Provider>
);
}
}
4 changes: 2 additions & 2 deletions src/components/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions src/runner/openscad-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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}`);
Expand Down
18 changes: 11 additions & 7 deletions src/state/fragment-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
66 changes: 43 additions & 23 deletions src/state/initial-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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) {
Expand All @@ -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,
});
}
35 changes: 23 additions & 12 deletions src/state/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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');
Expand Down
12 changes: 8 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,24 @@ 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);
const buffer = await response.arrayBuffer();
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}));
}
Expand Down
20 changes: 13 additions & 7 deletions tests/e2e.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const longTimeout = 60000;

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 = [];

Expand Down Expand Up @@ -33,10 +33,13 @@ afterEach(async () => {
});

function loadSrc(src) {
return page.goto(url + '#src:' + encodeURIComponent(src));
return page.goto(baseUrl + '#src=' + encodeURIComponent(src));
}
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() {
await page.waitForSelector('model-viewer');
Expand Down Expand Up @@ -77,7 +80,7 @@ function waitForLabel(text) {

describe('e2e', () => {
test('load the default page', async () => {
await page.goto(url);
await page.goto(baseUrl);
await waitForViewer();
expectObjectList();
}, longTimeout);
Expand Down Expand Up @@ -112,18 +115,21 @@ describe('e2e', () => {
expect3DPolySet();
}, 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 () => {
await loadSrc([
'myVar = 10;',
'cube(myVar);',
].join('\r\n'));
await waitForViewer();
expect3DPolySet();

await (await waitForCustomizeButton()).click();

await page.waitForSelector('fieldset');

await waitForLabel('myVar');
}, longTimeout);
});

0 comments on commit cc4cd1a

Please sign in to comment.