Skip to content

Commit 13cccbc

Browse files
authored
Replace the rerender event bus with direct ObjectsManager calls (#311)
* Manage the preview's rerender using events * Cleanups * POC: toggle extrusions and travel moves without re-rendering everyting * Add unit tests for EventsDispatcher and ObjectsManager classes Tests cover event registration/emission, visibility toggles, rendering methods, disposal, and clipping plane updates. * Replace the rerender event bus with direct ObjectsManager calls Property changes were routed through a string-keyed dispatcher into one debounced full rebuild. Nothing outside the library listened to any of them, so 21 of the 23 SceneManagerEvent members existed only to say "call a method on my own collaborator". Setters now call the ObjectsManager, which decides how to react: visibility flips group.visible, colors and lighting mutate material uniforms, layer ranges move clipping planes, and only the dimension properties discard geometry and ask for a redraw. SceneManagerEvent keeps the two real notifications, animationComplete and frameRendered. Fixes found along the way: - clipping planes were built from lineWidth/lineHeight instead of the layer range, and never applied to materials created later - every rerender added an empty LineSegments2 and leaked its material - tube materials were pushed once per progressive frame rather than cached per tool - renderPathIndex indexes job.paths but was slicing job.travels, so re-enabling travels after a completed render drew nothing - clear() left the old ObjectsManager in disposables, double-disposing * Hold the new code at 100% coverage Coverage was not measurable: no provider was installed. Adds @vitest/coverage-v8, an npm run coverage script, and 100% thresholds scoped to objects-manager, scene-manager and events-dispatcher. scene-manager sat at 23% because a real instance needs WebGL. Stubbing only WebGLRenderer and OrbitControls lets the tests build a genuine SceneManager and assert on actual scene state -- object identity across toggles, material uniforms, group visibility -- rather than on spies. * Stop tracking the workspace .context scratch directory It was swept in by a git add -A during the develop merge. It holds agent scratch files, not project files. * Remove EventsDispatcher entirely The string-keyed bus is gone along with its last four events. Each consumer now gets a typed callback instead: - SceneManager.onFrameRendered replaces frameRendered - GCodePreview.onJobUpdated replaces jobUpdated - GCodePreview.onStreamEnd replaces streamReadEnd - animationComplete is dropped; renderAnimated already returns a promise that resolves at the same moment Interpreter no longer takes a dispatcher at all. GCodePreview owns the execute() calls, so it announces job updates itself. This removes the public addEventListener API along with EventName and EventNameType. The demo assigns the callbacks directly. Also drops a stray console.log(this.eventsDispatcher) from Interpreter. * Pack line vertices into a pre-sized Float32Array renderPathsAsLines grew a plain array one push at a time and handed it to setPositions, which then copied the whole thing into a Float32Array. Counting segments first and filling the typed array in place skips both the repeated growth and the conversion. On a 7023 path model that is 652818 floats: 3.1ms -> 0.5ms, and about 5 MB of transient boxed doubles never allocated. Output is byte for byte identical. * Cover the remaining scene-manager branches after the merge Adds tests for rendering without a build volume, a rebuild request firing after clear, and construction without a build volume, keeping the scene-manager.ts coverage gate at 100%. * Reconcile the rebase with develop's newer work Rebasing onto develop dropped the two merge commits, and with them the manual resolutions they carried. This restores that work: - scene-manager-properties gets back the orthographic camera coverage added while merging develop's camera feature - renderer-smoke looks up the Extrusions group again - the scene-manager suite stays branch-side, as the old-architecture suite tests render paths that now live in ObjectsManager - the demo drops the unused drawBoundingBox ref It also adopts what develop gained since the last sync: the fallbackExtrusionColor guard stays in renderPaths (a tool index past the configured colors warns once and falls back instead of drawing with undefined), and its tests are ported to the new-architecture suite in scene-manager-properties. * Fix the three review blockers in the ObjectsManager rework - createColorMaterial returns a fresh ShaderMaterial instead of a module-global color-keyed cache. The cache made tools with the same starting color share one instance, so recoloring tool 0 repainted every alias, and a disposed material could be resurrected by the next manager with stale uniforms. ObjectsManager already caches per tool, which is the granularity the mutate-in-place design needs. - clear() carries renderTubes, ambientLight, directionalLight and brightness into the replacement manager. It only carried the dimensions, so loading a second file silently reverted tube rendering and lighting to defaults. - updateClippingPlanes computes an unset start layer as undefined instead of NaN. startLayer?.z - startLayer?.height is NaN when unset, which passed the !== undefined guards and baked NaN plane constants and clip uniforms into every material by default — NaN comparisons are undefined behavior in GLSL, so strict mobile drivers could discard everything. An open bound now stays -Infinity/Infinity. * Address the review's should-fix findings - A scalar extrusionColor now recolors every tool: setExtrusionColor without a tool index repaints all tube materials and all extrusion lines. Previously the setter defaulted to tool 0, so tools >= 1 kept their old color until an unrelated geometry rebuild. A color array shorter than the tool count likewise repaints the extra tools with the fallback color instead of leaving them stale. - The constructor no longer calls initGui() directly. The devMode setter two lines above already creates the GUI, so devMode previews got two DevGUI instances and the first was never destroyed. - processGCode returns the renderAnimated() promise and processGCodeStream awaits it, so callers can observe the moment the model is fully drawn. That promise is the documented replacement for the old animationComplete event, but neither entry point propagated it. - Job and SceneManagerOptions are exported from the entry point (the onJobUpdated callback takes a Job, which consumers could not name), and the three callback properties document their single-listener contract. * Export SceneManagerOptions as a type It is type-only, so re-exporting it as a value fails rollup's build. * Restore develop's formatting in the demo page The page had been fully rewrapped by an editor, drowning the PR's three real changes in whitespace noise. This rebuilds the file from develop's version with only those changes: the bounding box toggle binds to settings.drawBoundingBox, the dev mode widget is added, and the canvas drops its binding to the removed update handler. * Hold objects-manager.ts at 100% coverage Adds the file to the vitest thresholds now that its last four defensive branches are exercised: uniform-less materials in the all-tools recolor and lighting paths, non-line children in the travel group, and an open-ended layer range resetting the clip uniforms to infinities. * Leave the layer range unclipped at the ends of the stack A clipping bound that sits at the very end of the layer stack is no restriction at all, yet it still planted a plane exactly at the top extrusion layer. Travel moves routinely rise above that layer — a final park move, a wipe — so a preview with endLayer set to the layer count (what the demo does by default) silently hid them (#278). updateClippingPlanes now only bounds a side whose layer is strictly inside the stack: startLayer 1 and endLayer == countLayers produce no plane on their side. A genuinely restricted range still clips travel and extrusion lines alike, which is why the selective-clipping TODO in updateLineClipping is retired rather than implemented: hiding travels outside a restricted range is intended, and with no planes in the unrestricted case there is nothing left to exempt travels from. Fixes #278
1 parent 67aa4f1 commit 13cccbc

21 files changed

Lines changed: 2468 additions & 1301 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ docs
1313

1414
# Test coverage
1515
coverage
16+
.context

demo/index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ <h1 class="text-center m-3 mb-5">GCode Preview
217217
</div>
218218
<div class="controls">
219219
<label for="bounding-color">Bounding box</label>
220-
<input type="checkbox" v-model="drawBoundingBox" />
220+
<input type="checkbox" v-model="settings.drawBoundingBox" />
221221
</div>
222222
<div class="controls">
223223
<label for="bbx-color">Bounding box color</label>
@@ -249,11 +249,11 @@ <h1 class="text-center m-3 mb-5">GCode Preview
249249
</footer>
250250
</div>
251251
</div>
252+
<div class="widget top right glass p-3 is-size-7 slide-in-down border"><label class="mr-2">dev mode</label><input
253+
v-model="enableDevMode" type="checkbox" /> </div>
252254
<div class="absolute left bottom m-3 has-text-grey slide-in-up">Drop a gcode file to preview it</div>
253255
<div class="wrapper">
254-
<canvas class="preview" :class="{ 'dragging': settings.dragging }"
255-
@update.prevent="update"
256-
></canvas>
256+
<canvas class="preview" :class="{ 'dragging': settings.dragging }"></canvas>
257257
</div>
258258
</div>
259259
<script type="importmap">

demo/js/app.js

Lines changed: 46 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const app = (window.app = createApp({
2323
const model = ref(null);
2424
const dragging = ref(false);
2525
const settings = ref(Object.assign({}, defaultSettings));
26-
const drawBoundingBox = ref(false);
26+
const enableDevMode = ref(false);
2727

2828
watch(selectedPreset, (preset) => {
2929
selectPreset(preset);
@@ -35,31 +35,10 @@ export const app = (window.app = createApp({
3535

3636
const removeColor = () => settings.value.colors.pop();
3737

38-
const update = async (evt) => {
39-
model.value = {
40-
name: evt.detail.filename
41-
};
42-
updateUI();
43-
};
44-
4538
// Update UI with current preview settings
4639
const updateUI = async () => {
47-
const { parser, sceneManager, countLayers } = preview;
48-
const {
49-
topLayerColor,
50-
lastSegmentColor,
51-
buildVolume,
52-
singleLayerMode,
53-
renderTravel,
54-
travelColor,
55-
renderExtrusion,
56-
lineWidth,
57-
renderTubes,
58-
extrusionWidth,
59-
boundingBoxColor,
60-
extrusionColor,
61-
backgroundColor
62-
} = sceneManager;
40+
console.log('Updating UI');
41+
const { parser, countLayers, sceneManager } = preview;
6342
const { thumbnails } = parser.metadata;
6443

6544
// thumbnail.value = thumbnails['220x124']?.src;
@@ -70,33 +49,16 @@ export const app = (window.app = createApp({
7049
thumbnail.value = thumbnails[largestThumbnailKey]?.src;
7150

7251
layerCount.value = countLayers;
73-
const colors = extrusionColor instanceof Array ? extrusionColor : [extrusionColor];
52+
sceneManager.endLayer = countLayers;
53+
7454
const currentSettings = {
75-
startLayer: 1,
76-
enableStartLayer: false,
7755
maxLayer: countLayers || 1000,
78-
endLayer: countLayers,
79-
enableEndLayer: false,
80-
singleLayerMode,
81-
renderTravel,
82-
travelColor: '#' + travelColor.getHexString(),
83-
renderExtrusion,
84-
lineWidth,
85-
renderTubes,
86-
extrusionWidth,
87-
colors: colors.map((c) => '#' + c.getHexString()),
88-
topLayerColor: '#' + topLayerColor?.getHexString(),
89-
highlightTopLayer: !!topLayerColor,
90-
lastSegmentColor: '#' + lastSegmentColor?.getHexString(),
91-
highlightLastSegment: !!lastSegmentColor,
92-
buildVolume: buildVolume,
93-
drawBuildVolume: !!buildVolume,
94-
backgroundColor: '#' + backgroundColor.getHexString(),
95-
boundingBoxColor
56+
endLayer: countLayers
9657
};
97-
console.debug('app settings:', currentSettings);
58+
9859
Object.assign(settings.value, currentSettings);
99-
sceneManager.endLayer = countLayers;
60+
61+
applyDevMode(enableDevMode.value);
10062
};
10163

10264
const loadGCodeFromServer = async (filename) => {
@@ -109,62 +71,66 @@ export const app = (window.app = createApp({
10971
const gcodeStream = response.body.pipeThrough(new TextDecoderStream());
11072

11173
const prevDevMode = preview.devMode;
112-
// preview.clear();
11374
preview.devMode = prevDevMode;
11475

115-
await preview.processGCodeStream(gcodeStream, { render: false }); // rendering will be done reactively
116-
};
117-
118-
const render = async () => {
119-
if (loadProgressive.value && preview.job.layers !== null) {
120-
await preview.sceneManager.renderAnimated();
121-
} else {
122-
preview.render();
123-
}
76+
await preview.processGCodeStream(gcodeStream); // rendering will be done reactively
77+
updateUI();
12478
};
12579

12680
const selectPreset = async (presetName) => {
127-
const canvas = document.querySelector('canvas.preview');
81+
preview.clear();
12882
const preset = presets[presetName];
12983
model.value = preset.model;
13084

131-
// cascade settings: first defaults, then apply the preset, finally some overrides
13285
const options = {
13386
...defaultSettings,
134-
...preset,
135-
canvas,
136-
droppable: true,
137-
backgroundColor: initialBackgroundColor
87+
...preset
13888
};
89+
if (options.initialCameraPosition) {
90+
preview.sceneManager.camera.position.fromArray(options.initialCameraPosition);
91+
}
92+
93+
console.debug('Applying preset', presetName, options);
13994

140-
// update UI state
141-
drawBoundingBox.value = options.boundingBoxColor !== undefined;
95+
Object.assign(settings.value, options);
14296

14397
// reset previous state
14498
const lilGuiElement = document.querySelector('.lil-gui');
14599
if (lilGuiElement) document.body.removeChild(lilGuiElement);
146100
const stats = document.querySelector('.stats');
147101
if (stats) stats.parentNode.removeChild(stats);
148102
if (defaultSettings.devMode) defaultSettings.devMode.statsContainer = statsContainer();
149-
preview?.dispose();
150103

151-
window['_preview'] = preview = new GCodePreview(options);
104+
loadGCodeFromServer(preset.file);
105+
};
106+
107+
function applyDevMode(enabled) {
108+
// these elements will be recreated when changing presets, so we'll look them up dynamically
109+
document.querySelectorAll('.lil-gui, .stats').forEach((el) => (el.style.display = enabled ? 'block' : 'none'));
110+
}
111+
112+
watch(enableDevMode, applyDevMode);
113+
114+
onMounted(async () => {
115+
const canvas = document.querySelector('canvas.preview');
116+
117+
window['_preview'] = preview = new GCodePreview({
118+
...defaultSettings,
119+
canvas: canvas,
120+
droppable: true,
121+
backgroundColor: initialBackgroundColor
122+
});
152123

153124
// resize preview on canvas resize (TODO: move to GCodePreview)
154125
if (observer) observer.disconnect();
155126
observer = new ResizeObserver(() => preview.sceneManager.resize());
156127
observer.observe(canvas);
157128

158-
await loadGCodeFromServer(preset.file);
159-
160-
updateUI();
161-
};
162-
163-
onMounted(async () => {
164-
await selectPreset(defaultPreset);
129+
// to update the layer count and the thumbnail when available
130+
preview.onJobUpdated = () => updateUI();
131+
preview.onStreamEnd = () => updateUI();
165132

166133
watchEffect(() => {
167-
if (!preview) return;
168134
preview.sceneManager.backgroundColor = settings.value.backgroundColor;
169135

170136
if (preview.sceneManager.buildVolume && settings.value.drawBuildVolume) {
@@ -184,13 +150,12 @@ export const app = (window.app = createApp({
184150
} else if (preview.sceneManager.buildVolume && !settings.value.drawBuildVolume) {
185151
preview.sceneManager.buildVolume = undefined;
186152
}
187-
preview.sceneManager.boundingBoxColor = drawBoundingBox.value
188-
? (settings.value.boundingBoxColor ?? 'magenta')
153+
preview.sceneManager.boundingBoxColor = settings.value.drawBoundingBox
154+
? settings.value.boundingBoxColor
189155
: undefined;
190156
});
191157

192158
watchEffect(() => {
193-
if (!preview) return;
194159
preview.sceneManager.renderTravel = settings.value.renderTravel;
195160
preview.sceneManager.travelColor = settings.value.travelColor;
196161
preview.sceneManager.lineWidth = +settings.value.lineWidth;
@@ -205,16 +170,9 @@ export const app = (window.app = createApp({
205170
preview.sceneManager.lastSegmentColor = settings.value.highlightLastSegment
206171
? settings.value.lastSegmentColor
207172
: undefined;
208-
209-
// run render after settings have been applied
210-
// this is needed to prevent reactivity attaching the render function
211-
setTimeout(() => {
212-
render();
213-
}, 0);
214173
});
215174

216175
watchEffect(() => {
217-
if (!preview) return;
218176
const startLayer = parseIntOrDefault(settings.value.startLayer, undefined);
219177
const endLayer = parseIntOrDefault(settings.value.endLayer, undefined);
220178

@@ -223,7 +181,6 @@ export const app = (window.app = createApp({
223181
});
224182

225183
watchEffect(() => {
226-
if (!preview) return;
227184
preview.sceneManager.singleLayerMode = settings.value.singleLayerMode;
228185
});
229186

@@ -237,6 +194,8 @@ export const app = (window.app = createApp({
237194
preview.sceneManager.extrusionColor =
238195
settings.value.colors.length === 1 ? settings.value.colors[0] : settings.value.colors;
239196
});
197+
198+
selectPreset(defaultPreset);
240199
});
241200

242201
return {
@@ -250,11 +209,10 @@ export const app = (window.app = createApp({
250209
dragging,
251210
settings,
252211
loadProgressive,
253-
drawBoundingBox,
212+
enableDevMode,
254213
selectTab,
255214
addColor,
256215
removeColor,
257-
update,
258216
resetUI: updateUI,
259217
loadGCodeFromServer,
260218
selectPreset

demo/js/default-settings.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,7 @@ export const defaultSettings = {
3030
lastSegmentColor: null,
3131
drawBuildVolume: true,
3232
backgroundColor: '#141414',
33+
boundingBoxColor: '#A830F8',
34+
drawBoundingBox: false,
3335
orthographic: false
3436
};

demo/js/presets.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const presets = {
1010
},
1111
extrusionWidth: 0.45,
1212
lineHeight: 0.2,
13-
extrusionColor: ['#95dfa1'],
13+
colors: ['#95dfa1'],
1414
travelColor: 'red',
1515
buildVolume: {
1616
x: 180,
@@ -30,7 +30,7 @@ export const presets = {
3030
},
3131
extrusionWidth: 0.5,
3232
lineHeight: 0.3,
33-
extrusionColor: ['#95dfa1'],
33+
colors: ['#95dfa1'],
3434
travelColor: 'red',
3535
topLayerColor: undefined,
3636
lastSegmentColor: undefined,
@@ -55,7 +55,7 @@ export const presets = {
5555
minLayerThreshold: 0.6,
5656
renderExtrusion: true,
5757
renderTubes: true,
58-
extrusionColor: ['#8782bf'],
58+
colors: ['#8782bf'],
5959
renderTravel: true,
6060
travelColor: '#00FF00',
6161
topLayerColor: undefined,
@@ -80,7 +80,7 @@ export const presets = {
8080
lineHeight: 0.3,
8181
renderExtrusion: true,
8282
renderTubes: true,
83-
extrusionColor: ['#919191'],
83+
colors: ['#919191'],
8484
renderTravel: true,
8585
travelColor: '#00FF00',
8686
topLayerColor: '#aaaaaa',
@@ -101,7 +101,7 @@ export const presets = {
101101
original: 'https://www.thingiverse.com/thing:387266'
102102
},
103103
extrusionWidth: 0.5,
104-
extrusionColor: ['orange', 'black', 'white'],
104+
colors: ['orange', 'black', 'white'],
105105
travelColor: 'red',
106106
topLayerColor: undefined,
107107
lastSegmentColor: undefined,
@@ -122,7 +122,7 @@ export const presets = {
122122
},
123123
extrusionWidth: 0.45,
124124
lineHeight: 0.2,
125-
extrusionColor: ['pink'],
125+
colors: ['pink'],
126126
travelColor: 'red',
127127
topLayerColor: undefined,
128128
lastSegmentColor: undefined,
@@ -131,7 +131,8 @@ export const presets = {
131131
y: 180,
132132
z: 0
133133
},
134-
boundingBoxColor: 'pink'
134+
boundingBoxColor: 'pink',
135+
drawBoundingBox: true
135136
},
136137
easel: {
137138
title: 'Easel tool path (cnc)',

src/.eslintrc.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,14 @@ module.exports = {
44
ignorePatterns: ['three-line2'],
55
env: {
66
browser: true
7+
},
8+
rules: {
9+
'no-unused-vars': [
10+
'error',
11+
{
12+
argsIgnorePattern: '^_',
13+
ignoreRestSiblings: true
14+
}
15+
]
716
}
817
};

0 commit comments

Comments
 (0)