Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/firebase-hosting-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci && npm run build
- run: npm ci && npm run build:demo
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/firebase-hosting-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci && npm run build
- run: npm ci && npm run build:demo
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
Expand Down
7 changes: 0 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build
Expand Down
179 changes: 153 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,164 @@
# GCode Preview 3.0 with React & TypeScript
# gcode-preview-react

This demo uses [GCode Preview](https://github.com/xyz-tools/gcode-preview)
`3.0.0-alpha.6` with a React 19 / TypeScript / Vite setup.
A React component for [GCode Preview](https://github.com/xyz-tools/gcode-preview):
a 3D preview of G-code files, rendered with three.js into a `<canvas>`. It
creates and disposes the preview for you, loads a file from a URL or a string,
and keeps the preview in sync with its props.

Works with React 18 and 19, and in the Next.js App Router (the component is
marked `'use client'`).

## Install

```sh
npm install
npm start
npm install gcode-preview-react gcode-preview
```

Open http://localhost:5173. `npm run build` typechecks and creates the
production build in `build/`, which `npm run preview` serves. `npm run lint`
checks the source and `npm test` runs the lifecycle regression tests (Vitest).
`gcode-preview` (`^3.0.0-alpha.6`) and `react` are peer dependencies.

## Component
With TypeScript, also install `@types/three` to get strict types for the
color props (the core's option types come from three.js).

## Usage

```tsx
import GCodePreview from './components/GCodePreview';

<GCodePreview
src={`${import.meta.env.BASE_URL}square-tower.gcode`}
topLayerColor="lime"
lastSegmentColor="red"
startLayer={20}
endLayer={150}
/>
import { GCodePreview } from 'gcode-preview-react';

export function Viewer() {
return (
<GCodePreview
src="/benchy.gcode"
style={{ width: '100%', height: 400 }}
buildVolume={{ x: 250, y: 220, z: 150, smallGrid: true }}
topLayerColor="lime"
onError={error => console.warn(error.message)}
/>
);
}
```

The wrapper creates a `new GCodePreview(...)` after the canvas mounts and
passes a decoded fetch stream to `processGCodeStream`. Source or option changes
reload the preview. Layer ranges are applied through `preview.sceneManager`
after loading, when the complete layer count is known. Loading failures appear
below the canvas. Cleanup aborts fetch, removes the resize listener and calls
`preview.dispose()`, including during React Strict Mode remounts.
## Props

### Loading

| Prop | Type | Description |
| ------- | -------------------- | ---------------------------------------------------------- |
| `src` | `string \| URL` | URL of a G-code file. It is fetched and streamed in. |
| `gcode` | `string \| string[]` | G-code to process directly. If both are set, `gcode` wins. |

Changing `src` or `gcode` clears the preview and starts a new load; a load
that is still running is aborted. Unsetting both clears the preview. Pass a
stable array for `gcode` (for example from state or `useMemo`), since a new
array starts a new load.

### Preview options

Every option of the core `GCodePreview` (except `canvas`) is a prop, with the
type from `gcode-preview`. See the
[core documentation](https://gcode-preview.web.app/docs) for what each one
does. An option you don't pass is not passed to the core either, so the core
default applies.

**Live options** update the preview in place when the prop changes:

`buildVolume`, `backgroundColor`, `extrusionColor`, `travelColor`,
`topLayerColor`, `lastSegmentColor`, `boundingBoxColor`, `startLayer`,
`endLayer`, `renderExtrusion`, `renderTravel`, `renderTubes`, `lineWidth`,
`lineHeight`, `extrusionWidth`, `disableGradient`, `orthographic`, `devMode`

Only options whose value changed are applied. Objects and arrays such as
`buildVolume` and `extrusionColor` are compared one level deep, so passing
them inline is fine. `startLayer` and `endLayer` are applied again after each
load, because the core clamps them to the loaded layer count.

Changing a live option back to `undefined` resets it for `startLayer`,
`endLayer`, `topLayerColor`, `lastSegmentColor`, `boundingBoxColor`,
`buildVolume`, `lineHeight`, `extrusionWidth` and `devMode`. The other live
options keep their last value, because their core setters don't accept
`undefined`.

**Mount-only options** are read once, when the preview is created. Later
changes are ignored; remount the component (for example with a `key`) to
apply them:

`initialCameraPosition`, `minLayerThreshold`, `droppable`, `keepLines`,
`liveRenderInterval`, `arcChordTolerance`

### Canvas attributes

Any other canvas attribute (`className`, `style`, `id`, `tabIndex`, `aria-*`,
event handlers, ...) is passed to the `<canvas>`. It has
`aria-label="G-code preview"` unless you pass your own.

## Callbacks

| Prop | Called with | When |
| --------- | --------------------------- | ----------------------------------------------- |
| `onReady` | the `GCodePreview` instance | the preview has been created |
| `onLoad` | the `GCodePreview` instance | `src` or `gcode` has been loaded and rendered |
| `onError` | an `Error` | loading failed, e.g. `HTTP 404: /missing.gcode` |

Without an `onError` prop, load errors are logged with `console.error`. A load
that was replaced or whose component unmounted calls nothing. The latest
callbacks are always used, so inline functions don't recreate the preview.

## Instance access

The `ref` gives you the core `GCodePreview` instance, for anything the props
don't cover. It is set after mount (use it in an effect, an event handler or
`onReady`), and `null` after unmount.

```tsx
import type { GCodePreview as Preview } from 'gcode-preview';
import { GCodePreview } from 'gcode-preview-react';
import { useRef } from 'react';

function Viewer() {
const preview = useRef<Preview>(null);
return (
<>
<GCodePreview ref={preview} src="/benchy.gcode" />
<button onClick={() => preview.current?.sceneManager.render()}>
Render
</button>
</>
);
}
```

## Sizing

The component renders a bare `<canvas>` without any size or stylesheet: size
the canvas with CSS (`className` or `style`). The preview follows the canvas
size through a `ResizeObserver`.

## Development

```sh
npm install
npm run dev
```

`npm run dev` starts the demo app (`src/App.tsx`) at http://localhost:5173. The
demo imports the component from source (`src/lib`). `npm run build:demo`
typechecks and builds the demo into `build/`, which is deployed to Firebase
Hosting.

| Script | What it does |
| ---------------------- | ---------------------------------------------- |
| `npm run build` | builds the library (`src/lib`) into `dist/` |
| `npm test` | runs the component tests (Vitest, core mocked) |
| `npm run typecheck` | typechecks the demo, tests and library |
| `npm run lint` | ESLint + Prettier |
| `npm run lint:package` | checks the package setup with publint |

The package is marked `"private": true` so it can't be published by accident.
Remove that flag when it is ready to be published; `prepack` builds the
library first.

The square and triangle towers in `public/` are small synthetic samples, not
printer-ready G-code.

## License

The local square and triangle towers are small synthetic preview samples, not
printer-ready G-code. Put your own files in `public/` and change `src` to use them.
MIT
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import globals from 'globals';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{ ignores: ['build', 'coverage'] },
{ ignores: ['build', 'dist', 'coverage'] },
js.configs.recommended,
tseslint.configs.recommended,
react.configs.flat.recommended,
Expand Down
12 changes: 2 additions & 10 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,11 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="GCode Preview 3.0 demo built with React, TypeScript and Vite"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>gcode-preview-react-typescript-demo</title>
<meta name="description" content="Live demo of gcode-preview-react" />
<title>GCode Preview React demo</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
Expand Down
Loading
Loading