diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 7867d695..f5be2e4c 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -1003,6 +1003,7 @@ export const conf = { widget: {}, // legacy localStorage settings (like octo print) local: { + 'model.opacity': 1.0, 'model.edges': false, 'cam.anim.trans': true, 'cam.anim.model': false, diff --git a/src/kiri/app/function.js b/src/kiri/app/function.js index ac3c4904..2a6f5e0d 100644 --- a/src/kiri/app/function.js +++ b/src/kiri/app/function.js @@ -290,7 +290,7 @@ function prepareSlices(callback, scale = 1, offset = 0) { if (scale === 1) { // clear wireframe const scheme = getColorScheme(); - widget.setWireframe(false, scheme.wireframe.color, scheme.wireframe.opacity); + widget._setWireframe(false, scheme.wireframe.color, scheme.wireframe.opacity); widget.setOpacity(camOrLaser ? scheme.views.SLICE.sliced_opacity ?? 0.2 : scheme.views.PREVIEW?.sliced_opacity ?? 0.0); widget.setColor(scheme.widget.deselected); api.hide.alert(alert); diff --git a/src/kiri/app/init/sync.js b/src/kiri/app/init/sync.js index b507c121..a5ed5915 100644 --- a/src/kiri/app/init/sync.js +++ b/src/kiri/app/init/sync.js @@ -54,6 +54,19 @@ export async function init_sync() { console.log({octoprint:ohost}); } + // One-time migration: Initialize model.opacity from existing ghosting state + if (api.local.getFloat('model.opacity') === null) { + const wireframeOpacity = api.local.getFloat('model.wireframe.opacity'); + const wireframe = api.local.getBoolean('model.wireframe', false); + + // If ghosting was enabled (wireframe off, opacity < 1), preserve it + if (!wireframe && wireframeOpacity !== null && wireframeOpacity < 1) { + api.local.set('model.opacity', wireframeOpacity); + } else { + api.local.set('model.opacity', 1.0); + } + } + // load workspace from url if (SETUP.wrk) { set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); @@ -262,9 +275,26 @@ function setup_keybd_nav() { // rendering options $('render-edges').onclick = () => { api.view.set_edges({ toggle: true }); api.conf.save() }; - $('render-ghost').onclick = () => { api.view.set_wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; - $('render-wire').onclick = () => { api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; - $('render-solid').onclick = () => { api.view.set_wireframe(false, 0, 1); }; + $('render-ghost').onclick = () => { + const opacity = api.view.is_arrange() ? 0.4 : 0.25; + api.view.set_wireframe(false); + api.visuals.set_opacity(opacity); + // Also save to old key for backwards compatibility + api.local.set('model.wireframe.opacity', opacity); + api.conf.save(); + }; + $('render-wire').onclick = () => { + api.view.set_wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); + api.visuals.set_opacity(1.0); + api.conf.save(); + }; + $('render-solid').onclick = () => { + api.view.set_wireframe(false); + api.visuals.set_opacity(1.0); + // Also save to old key for backwards compatibility + api.local.set('model.wireframe.opacity', 1.0); + api.conf.save(); + }; $('mesh-export-stl').onclick = () => { settingsOps.export_objects('stl') }; $('mesh-export-obj').onclick = () => { settingsOps.export_objects('obj') }; $('mesh-merge').onclick = selection.merge; diff --git a/src/kiri/app/keyboard.js b/src/kiri/app/keyboard.js index c68df9f6..7d6e49fc 100644 --- a/src/kiri/app/keyboard.js +++ b/src/kiri/app/keyboard.js @@ -264,7 +264,7 @@ class KeyboardControl { let api = this.#api; let isWireframe = api.local.getBoolean('model.wireframe'); - let isGhost = api.local.getFloat('model.wireframe.opacity') < 1; + let isGhost = !isWireframe && (api.local.getFloat('model.opacity') ?? 1.0) < 1; switch (evt.keyCode) { case this.#cca('`'): this.#api.show.slices(0); break; diff --git a/src/kiri/app/local.js b/src/kiri/app/local.js index 9d5d3335..3afba384 100644 --- a/src/kiri/app/local.js +++ b/src/kiri/app/local.js @@ -38,7 +38,12 @@ export const local = { get: (key) => localGet(key), getItem: (key) => localGet(key), getInt: (key) => parseInt(localGet(key)), - getFloat: (key) => parseFloat(localGet(key)), + getFloat: (key) => { + const val = localGet(key); + if (val === undefined || val === null) return null; + const parsed = parseFloat(val); + return isNaN(parsed) ? null : parsed; + }, getBoolean: localGetBoolean, toggle: (key, val, def) => { return localSet(key, val ?? !localGetBoolean(key, def)); diff --git a/src/kiri/app/state/visual.js b/src/kiri/app/state/visual.js index 81807d0e..ca2f6429 100644 --- a/src/kiri/app/state/visual.js +++ b/src/kiri/app/state/visual.js @@ -190,8 +190,8 @@ class VisualStateManager { */ setVisualState({ edges, wires, opacity }) { this.widget.cache.vizstate = this.getVisualState(); - this.widget.setEdges(edges ?? false); - this.widget.setWireframe(wires ?? false); + this.widget._setEdges(edges ?? false); + this.widget._setWireframe(wires ?? false); if (opacity !== undefined) { this.widget.setOpacity(opacity); } diff --git a/src/kiri/app/view-mode.js b/src/kiri/app/view-mode.js index dac5e95d..dbb87dc9 100644 --- a/src/kiri/app/view-mode.js +++ b/src/kiri/app/view-mode.js @@ -54,28 +54,27 @@ function setViewMode(mode) { api.visuals.update_speeds(); api.visuals.set_visible_layer(); api.visuals.set_widget_visibility(true); - api.view.set_edges(api.local.getBoolean('model.edges')); - api.view.set_wireframe(api.local.getBoolean('model.wireframe')); - // Only set opacity to 1 if wireframe is disabled - if (!api.local.getBoolean('model.wireframe')) { - api.widgets.setOpacity(1); - } + // Restore global visual state (opacity, edges, wireframe) + api.widgets.each(w => w.applyGlobalVisualState()); break; case VIEWS.SLICE: $('act-slice').classList.add('selected'); api.visuals.update_speeds(); api.visuals.update_slider_max(); api.visuals.set_widget_visibility(true); - !isCAM && api.view.set_edges(false); + // Hide edges without persisting (preserve user preference) + !isCAM && api.view.set_edges(false, false); break; case VIEWS.PREVIEW: $('act-preview').classList.add('selected'); api.visuals.set_widget_visibility(true); - !isCAM && api.view.set_edges(false); + // Hide edges without persisting (preserve user preference) + !isCAM && api.view.set_edges(false, false); break; case VIEWS.ANIMATE: $('act-animate').classList.add('selected'); - !isCAM && api.view.set_edges(false); + // Hide edges without persisting (preserve user preference) + !isCAM && api.view.set_edges(false, false); break; default: console.log("invalid view mode: "+mode); diff --git a/src/kiri/app/visuals.js b/src/kiri/app/visuals.js index b34e8c41..a5f0af73 100644 --- a/src/kiri/app/visuals.js +++ b/src/kiri/app/visuals.js @@ -114,6 +114,25 @@ function setVisibleLayer(h, l) { showSlices(); } +/** + * Set global opacity for all widgets. + * Updates material opacity for all widgets and persists to localStorage. + * This is the primary opacity control - all widgets share the same opacity. + * @param {number} value - Opacity value (0.0 to 1.0) + */ +function setOpacity(value) { + api.local.set('model.opacity', value); + // Also update old key for backwards compatibility + api.local.set('model.wireframe.opacity', value); + api.widgets.each(w => { + const mat = w.getMaterial(); + mat.opacity = value; + mat.transparent = value < 1.0; + mat.visible = value > 0.0; + }); + space.update(); +} + /** * Set wireframe rendering mode for all widgets. * Updates 3D scene after applying to all widgets. @@ -144,7 +163,7 @@ function setWireframe(bool, color, opacity) { const saved = api.local.getFloat('model.wireframe.opacity'); opacity = saved !== null ? saved : (api.space.is_dark() ? 0.25 : 0.5); } - api.widgets.each((w) => { w.setWireframe(bool, color, opacity) }); + api.widgets.each((w) => { w._setWireframe(bool, color, opacity) }); space.update(); } @@ -154,15 +173,18 @@ function setWireframe(bool, color, opacity) { * Otherwise sets edge visibility to bool value. * Persists state to local storage and updates scene. * @param {boolean|object} bool - Enable/disable edges, or {toggle: true} to toggle + * @param {boolean} [persist=true] - Whether to save state to local storage */ -function setEdges(bool) { +function setEdges(bool, persist = true) { if (bool && bool.toggle) { api.local.toggle('model.edges'); + bool = api.local.getBoolean('model.edges'); } else { - api.local.set('model.edges', bool); + if (persist) { + api.local.set('model.edges', bool); + } } - bool = api.local.getBoolean('model.edges'); - api.widgets.each(w => w.setEdges(bool)); + api.widgets.each(w => w._setEdges(bool)); space.update(); } @@ -188,19 +210,8 @@ function updateSliderMax(set) { */ function hideSlices() { STACKS.clear(); - // Restore saved wireframe state instead of forcing wireframe off - const wireframe = api.local.getBoolean('model.wireframe'); - const color = api.local.getInt('model.wireframe.color') || 0; - const saved = api.local.getFloat('model.wireframe.opacity'); - const scheme = getColorScheme(); - const opacity = saved !== null ? saved : (scheme.views.ARRANGE?.model_opacity ?? 1.0); - - if (wireframe) { - api.widgets.each(widget => widget.setWireframe(wireframe, color, opacity)); - } else { - api.widgets.setOpacity(opacity); - api.widgets.each(widget => widget.setWireframe(false)); - } + // Restore global visual state + api.widgets.each(widget => widget.applyGlobalVisualState()); } /** @@ -284,7 +295,7 @@ function updateStackLabelState() { /** * Apply saved visual state to a widget. - * Restores edges and wireframe settings from local storage. + * Delegates to widget's applyGlobalVisualState method. * Only applies in ARRANGE view mode. * @param {Widget} widget - Widget to apply visual state to */ @@ -294,25 +305,7 @@ function applyVisualState(widget) { if (viewMode !== VIEWS.ARRANGE) { return; } - const edges = api.local.getBoolean('model.edges'); - const wireframe = api.local.getBoolean('model.wireframe'); - - // Always apply edges setting - if (edges) { - widget.setEdges(edges); - } - - // Apply wireframe and its associated opacity - const color = api.local.getInt('model.wireframe.color') || 0; - const saved = api.local.getFloat('model.wireframe.opacity'); - const opacity = saved !== null ? saved : 1; - - if (wireframe) { - widget.setWireframe(wireframe, color, opacity); - } else if (saved !== null) { - // Even if wireframe is off, restore the saved opacity - widget.setOpacity(opacity); - } + widget.applyGlobalVisualState(); } /** @@ -381,6 +374,7 @@ export const visuals = { hide_slices: hideSlices, hide_slider: hideSlider, set_edges: setEdges, + set_opacity: setOpacity, set_progress: setProgress, set_visible_layer: setVisibleLayer, set_widget_visibility: setWidgetVisibility, diff --git a/src/kiri/app/widget.js b/src/kiri/app/widget.js index a6ae0a8a..487fded3 100644 --- a/src/kiri/app/widget.js +++ b/src/kiri/app/widget.js @@ -148,9 +148,12 @@ class Widget extends WidgetCore { */ setVisualState({ edges, wires, opacity}) { this.cache.vizstate = this.getVisualState(); - this.setEdges(edges ?? false); - this.setWireframe(wires ?? false); - this.setOpacity(opacity ?? 1); + this._setEdges(edges ?? false); + this._setWireframe(wires ?? false); + // Only set opacity if explicitly provided (don't default to 1) + if (opacity !== undefined) { + this.setOpacity(opacity); + } } /** @@ -169,6 +172,39 @@ class Widget extends WidgetCore { } } + /** + * Apply global visual state from localStorage to this widget. + * Reads opacity, edges, and wireframe settings from global config. + * This is the primary method for syncing widget appearance with global preferences. + */ + applyGlobalVisualState() { + if (!this.api) { + return; + } + + const material = this.getMaterial(); + + // Apply global opacity + const opacity = this.api.local.getFloat('model.opacity') ?? 1.0; + material.opacity = opacity; + material.transparent = opacity < 1.0; + material.visible = opacity > 0.0; + + // Apply global edges + const edges = this.api.local.getBoolean('model.edges', false); + this._setEdges(edges); + + // Apply global wireframe + const wireframe = this.api.local.getBoolean('model.wireframe', false); + if (wireframe) { + const color = this.api.local.getInt('model.wireframe.color') || 0; + const wfOpacity = this.api.local.getFloat('model.wireframe.opacity') ?? 0.5; + this._setWireframe(true, color, wfOpacity); + } else { + this._setWireframe(false); + } + } + /** * Set material opacity */ @@ -185,16 +221,23 @@ class Widget extends WidgetCore { } } + /** + * Set edge rendering - public interface for core widget rotation/scale + * Delegates to internal _setEdges method + */ setEdges(set) { - clearTimeout(this._setimer); - if (set === true && this.outline) { - this._setimer = setTimeout(() => this._setEdges(set), 10); - } else { - this._setEdges(set); - } + this._setEdges(set); } + /** + * Set edge rendering (internal method - use api.view.set_edges for global control) + * @private + */ _setEdges(set) { + this.__setEdges(set); + } + + __setEdges(set) { if (!(this.api && this.api.conf)) { // missing api features in engine mode return; @@ -253,15 +296,26 @@ class Widget extends WidgetCore { } /** - * Set wireframe rendering + * Set wireframe rendering - public interface for core widget rotation/scale + * Delegates to internal _setWireframe method */ setWireframe(set, color, opacity) { + this._setWireframe(set, color, opacity); + } + + /** + * Set wireframe rendering (internal method - use api.view.set_wireframe for global control) + * @private + */ + _setWireframe(set, color, opacity) { if (!(this.api && this.api.conf)) { // missing api features in engine mode return; } let mesh = this.mesh, widget = this; + // Save current opacity before removing wire + const currentOpacity = this.getMaterial().opacity; if (this.wire) { this.setOpacity(solid_opacity); mesh.remove(this.wire); @@ -287,6 +341,9 @@ class Widget extends WidgetCore { } if (opacity !== undefined) { widget.setOpacity(opacity); + } else if (!set && this.wire === null) { + // Restore original opacity when removing wireframe without explicit opacity + widget.setOpacity(currentOpacity); } }