refactor color and opacity/edge management

This commit is contained in:
Stewart Allen 2026-01-08 14:35:58 -05:00
commit 9900161dcd
17 changed files with 795 additions and 64 deletions

View file

@ -10,7 +10,7 @@ import web from '../../moto/webui.js';
import { beta, version } from '../../moto/license.js';
import { broker } from '../../moto/broker.js';
import { client as workers } from './workers.js';
import { consts, COLOR as color, LISTS as lists } from '../core/consts.js';
import { consts, LISTS as lists } from '../core/consts.js';
import { device, devices } from './devices.js';
import { functions } from './function.js';
import { group as groupModule } from './groups.js';
@ -113,7 +113,6 @@ export const api = {
.catch(err => console.error('Clipboard Error:', err));
},
clone: Object.clone,
color,
conf: settings.conf,
const: {
LANG, LOCAL, SETUP, SECURE, SPACE, STACKS,

View file

@ -0,0 +1,69 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* BASE_SCHEME: Shared colors used across all device modes
* All mode-specific schemes inherit from this base
*/
const BASE_SCHEME = {
// Widget colors for selected/deselected states
widget: {
selected: {
light: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ],
dark: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ]
},
deselected: {
light: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ],
dark: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ]
},
disabled: {
// Computed via avgc(0x888888, baseColor, 3)
mixWith: 0x888888,
mixRatio: 3
}
},
// Edge rendering settings
edges: {
color: {
light: 0x888888,
dark: 0x444444
},
angle: 20 // Default edge detection angle threshold
},
// Wireframe rendering settings
wireframe: {
color: {
light: 0x000000,
dark: 0xaaaaaa
},
opacity: {
light: 0.5,
dark: 0.25
}
},
// Grid colors for platform
grid: {
major: {
light: 0x999999,
dark: 0x666666
},
minor: {
light: 0xcccccc,
dark: 0x333333
}
},
// View-specific opacity defaults (common across modes)
views: {
ARRANGE: {
model_opacity: 1.0
},
SLICE: {
slicing_opacity: 0.5
}
}
};
export { BASE_SCHEME };

58
src/kiri/app/color/cam.js Normal file
View file

@ -0,0 +1,58 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { BASE_SCHEME } from './base.js';
/**
* CAM_SCHEME: Colors specific to CAM (3-axis milling/machining) mode
* Inherits from BASE_SCHEME and overrides/extends with CAM-specific values
*/
const CAM_SCHEME = {
// Inherit all base colors
...BASE_SCHEME,
// CAM-specific view overrides
views: {
...BASE_SCHEME.views,
SLICE: {
...BASE_SCHEME.views.SLICE,
sliced_opacity: 0.2 // CAM shows more during slice (vs FDM 0.0)
},
PREVIEW: {
preview: {
light: 0xdddddd,
dark: 0x888888
},
preview_opacity: {
light: 0.2,
dark: 0.2
}
}
},
// CAM-specific operations
operations: {
// Tab visualization (persistent geometry)
tabs: {
color: {
light: 0x0000dd, // Blue in light mode
dark: 0x00ddff // Cyan in dark mode
},
opacity: {
light: 0.6,
dark: 0.75
}
}
},
// Gcode preview colors (same as FDM, but documented for CAM)
gcode: {
head: 0x888888,
move: {
light: 0xaaaaaa,
dark: 0x666666
},
print: 0x777700
}
};
export { CAM_SCHEME };

42
src/kiri/app/color/fdm.js Normal file
View file

@ -0,0 +1,42 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { BASE_SCHEME } from './base.js';
/**
* FDM_SCHEME: Colors specific to FDM (Fused Deposition Modeling) mode
* Inherits from BASE_SCHEME and overrides/extends with FDM-specific values
*/
const FDM_SCHEME = {
// Inherit all base colors
...BASE_SCHEME,
// FDM-specific view overrides
views: {
...BASE_SCHEME.views,
PREVIEW: {
preview_opacity: 0.0, // FDM hides widget in preview
sliced_opacity: 0.0
}
},
// FDM-specific operations
operations: {
// Support painting overlay
paint: {
overlay: 0x4488ff, // Light blue for painted support regions
opacity: 0.7
}
},
// Gcode preview colors
gcode: {
head: 0x888888,
move: {
light: 0xaaaaaa,
dark: 0x666666
},
print: 0x777700
}
};
export { FDM_SCHEME };

View file

@ -0,0 +1,28 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { BASE_SCHEME } from './base.js';
/**
* LASER_SCHEME: Colors for LASER, DRAG (knife), WJET (waterjet), and WEDM (wire-edm) modes
* All 2D device types share similar visual characteristics
* Inherits from BASE_SCHEME with minimal overrides
*/
const LASER_SCHEME = {
// Inherit all base colors
...BASE_SCHEME,
// Laser-specific view overrides (similar to CAM)
views: {
...BASE_SCHEME.views,
PREVIEW: {
preview_opacity: 0.2
}
}
};
// DRAG, WJET, and WEDM all use the same scheme as LASER
const DRAG_SCHEME = LASER_SCHEME;
const WJET_SCHEME = LASER_SCHEME;
const WEDM_SCHEME = LASER_SCHEME;
export { LASER_SCHEME, DRAG_SCHEME, WJET_SCHEME, WEDM_SCHEME };

View file

@ -0,0 +1,145 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { FDM_SCHEME } from './fdm.js';
import { CAM_SCHEME } from './cam.js';
import { SLA_SCHEME } from './sla.js';
import { LASER_SCHEME, DRAG_SCHEME, WJET_SCHEME, WEDM_SCHEME } from './laser.js';
/**
* ColorSchemeRegistry: Central registry for all mode-specific color schemes
* Provides theme-aware color resolution for UI rendering
*/
class ColorSchemeRegistry {
constructor() {
// Map mode IDs to their schemes
this.schemes = {
1: FDM_SCHEME, // FDM mode
2: LASER_SCHEME, // LASER mode
3: CAM_SCHEME, // CAM mode
4: SLA_SCHEME, // SLA mode
5: DRAG_SCHEME, // DRAG knife mode
6: WJET_SCHEME, // Waterjet mode
7: WEDM_SCHEME // Wire-EDM mode
};
}
/**
* Get the complete scheme for a given mode and theme
* @param {number|string} mode - Mode ID (1=FDM, 2=LASER, 3=CAM, etc) or name
* @param {string} theme - 'light' or 'dark'
* @returns {object} Resolved scheme object
*/
getScheme(mode, theme = 'light') {
// Convert mode name to ID if needed
if (typeof mode === 'string') {
mode = this._modeNameToId(mode);
}
const scheme = this.schemes[mode];
if (!scheme) {
console.warn(`Unknown mode: ${mode}, using FDM scheme as fallback`);
return this._resolveTheme(this.schemes[1], theme);
}
return this._resolveTheme(scheme, theme);
}
/**
* Get a specific color value from a scheme
* @param {number|string} mode - Mode ID or name
* @param {string} theme - 'light' or 'dark'
* @param {string} path - Dot-separated path (e.g., 'widget.selected', 'edges.color')
* @returns {*} Color value or object
*/
getColor(mode, theme, path) {
const scheme = this.getScheme(mode, theme);
return this._getNestedValue(scheme, path);
}
/**
* Compute disabled color by mixing base color with gray
* Implements: avgc(0x888888, baseColor, 3)
* @param {number} baseColor - Base color hex value
* @returns {number} Mixed color hex value
*/
computeDisabledColor(baseColor) {
// Extract RGB components
const r1 = (0x888888 >> 16) & 0xFF;
const g1 = (0x888888 >> 8) & 0xFF;
const b1 = 0x888888 & 0xFF;
const r2 = (baseColor >> 16) & 0xFF;
const g2 = (baseColor >> 8) & 0xFF;
const b2 = baseColor & 0xFF;
// Average with ratio 3:1 (3 parts gray, 1 part base)
const r = Math.floor((r1 * 3 + r2) / 4);
const g = Math.floor((g1 * 3 + g2) / 4);
const b = Math.floor((b1 * 3 + b2) / 4);
return (r << 16) | (g << 8) | b;
}
/**
* Resolve theme-specific values throughout the scheme
* @private
*/
_resolveTheme(scheme, theme) {
const resolved = JSON.parse(JSON.stringify(scheme)); // Deep clone
this._resolveThemeRecursive(resolved, theme);
return resolved;
}
/**
* Recursively resolve theme values in an object
* @private
*/
_resolveThemeRecursive(obj, theme) {
for (const key in obj) {
const value = obj[key];
if (value && typeof value === 'object') {
// Check if this object has light/dark properties
if ('light' in value && 'dark' in value) {
// Replace the object with the theme-specific value
obj[key] = value[theme];
} else {
// Recurse into nested objects
this._resolveThemeRecursive(value, theme);
}
}
}
}
/**
* Get nested value from object using dot-separated path
* @private
*/
_getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
/**
* Convert mode name to ID
* @private
*/
_modeNameToId(name) {
const modes = {
'FDM': 1,
'LASER': 2,
'CAM': 3,
'SLA': 4,
'DRAG': 5,
'WJET': 6,
'WEDM': 7
};
return modes[name.toUpperCase()] || 1; // Default to FDM
}
}
// Create singleton instance
const colorSchemeRegistry = new ColorSchemeRegistry();
export { colorSchemeRegistry, ColorSchemeRegistry };

23
src/kiri/app/color/sla.js Normal file
View file

@ -0,0 +1,23 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { BASE_SCHEME } from './base.js';
/**
* SLA_SCHEME: Colors specific to SLA (stereolithography/resin) mode
* Inherits from BASE_SCHEME with minimal overrides
*/
const SLA_SCHEME = {
// Inherit all base colors
...BASE_SCHEME,
// SLA-specific view overrides (similar to FDM)
views: {
...BASE_SCHEME.views,
PREVIEW: {
preview_opacity: 0.0,
sliced_opacity: 0.0
}
}
};
export { SLA_SCHEME };

View file

@ -12,10 +12,20 @@ import { utils } from '../../core/utils.js';
import { version } from '../../../moto/license.js';
import { JSZip } from '../../../ext/jszip-esm.js';
import { colorSchemeRegistry } from '../color/schemes.js';
const { areEqual, ls2o, js2o } = utils;
const { COLOR } = consts;
const { clone } = Object;
/**
* Get current color scheme
*/
function getColorScheme() {
const mode = api.mode.get_id();
const theme = api.space.is_dark() ? 'dark' : 'light';
return colorSchemeRegistry.getScheme(mode, theme);
}
const localFilterKey ='kiri-gcode-filters';
const localFilters = js2o(local.getItem(localFilterKey)) || [];
@ -322,9 +332,10 @@ function updateExtruderFields(device) {
onclick() {
api.selection.for_widgets(w => {
w.anno.extruder = i;
const scheme = getColorScheme();
w.setColor(
api.selection.contains(w) ?
COLOR.selected : COLOR.deselected,
scheme.widget.selected : scheme.widget.deselected,
undefined,
false);
});
@ -332,7 +343,7 @@ function updateExtruderFields(device) {
}),
h.div({
class: "splat",
style: `background-color: #${COLOR.deselected[i].toString(16)}`
style: `background-color: #${getColorScheme().widget.deselected[i].toString(16)}`
})
])));
} else {

View file

@ -4,9 +4,19 @@ import { api } from './api.js';
import { client } from './workers.js';
import { codec } from '../core/codec.js';
import { space } from '../../moto/space.js';
import { COLOR, PMODES } from '../core/consts.js';
import { PMODES } from '../core/consts.js';
import { colorSchemeRegistry } from './color/schemes.js';
import { exportFile } from './export.js';
/**
* Get current color scheme
*/
function getColorScheme() {
const mode = api.mode.get_id();
const theme = api.space.is_dark() ? 'dark' : 'light';
return colorSchemeRegistry.getScheme(mode, theme);
}
/**
* Tracks completion state of operations to prevent redundant work.
* Properties: slice, preview, export
@ -96,7 +106,8 @@ function prepareSlices(callback, scale = 1, offset = 0) {
let totalProgress;
api.widgets.setOpacity(COLOR.slicing_opacity);
const scheme = getColorScheme();
api.widgets.setOpacity(scheme.views.SLICE.slicing_opacity);
let segtimes = {},
segNumber = 0,
@ -110,7 +121,8 @@ function prepareSlices(callback, scale = 1, offset = 0) {
for (let widget of toSlice) {
widget.belt = null;
widget.stats.progress = 0;
widget.setColor(COLOR.slicing);
const scheme = getColorScheme();
widget.setColor(0xffaaaa); // slicing color (from old COLOR.slicing)
let { extruder } = widget.anno;
if (extruder >= 0) {
extruders[extruder] = extruder;
@ -277,9 +289,10 @@ function prepareSlices(callback, scale = 1, offset = 0) {
}
if (scale === 1) {
// clear wireframe
widget.setWireframe(false, COLOR.wireframe, COLOR.wireframe_opacity);
widget.setOpacity(camOrLaser ? COLOR.cam_sliced_opacity : COLOR.sliced_opacity);
widget.setColor(COLOR.deselected);
const scheme = getColorScheme();
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);
}
}
@ -365,10 +378,12 @@ function preparePreview(callback, scale = 1, offset = 0) {
event.emit('preview.begin', pMode);
if (isCam) {
api.widgets.setOpacity(isDark ? COLOR.cam_preview_opacity_dark : COLOR.cam_preview_opacity);
api.widgets.setColor(isDark ? COLOR.cam_preview_dark : COLOR.cam_preview);
const scheme = getColorScheme();
api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity);
api.widgets.setColor(scheme.views.PREVIEW.preview);
} else if (offset === 0) {
api.widgets.setOpacity(COLOR.preview_opacity);
const scheme = getColorScheme();
api.widgets.setOpacity(scheme.views.PREVIEW.preview_opacity);
}
let mark = Date.now(),

View file

@ -3,7 +3,8 @@
import { $, h } from '../../moto/webui.js';
import { ajax, js2o } from '../core/utils.js';
import { api } from './api.js';
import { COLOR, MODES } from '../core/consts.js';
import { MODES } from '../core/consts.js';
import { colorSchemeRegistry } from './color/schemes.js';
import { load as file_load } from '../../load/file.js';
import { newBounds } from '../../geo/bounds.js';
import { Packer } from './pack.js';
@ -13,6 +14,15 @@ import { Widget, newWidget } from './widget.js';
const V0 = new THREE.Vector3(0,0,0);
/**
* Get current color scheme
*/
function getColorScheme() {
const mode = api.mode.get_id();
const theme = api.space.is_dark() ? 'dark' : 'light';
return colorSchemeRegistry.getScheme(mode, theme);
}
/** Bounds update callback */
let setbounds = undefined;
@ -154,17 +164,18 @@ function update_size(updateDark = true) {
gridMinor = unitMM ? 5 : 25.4 / 10;
if (updateDark) {
const scheme = getColorScheme();
if (controller.dark) {
space.platform.set({ light: 0.08 });
space.platform.setFont({rulerColor:'#888888'});
space.platform.setGrid(gridMajor, gridMinor, 0x666666, 0x333333);
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
space.platform.opacity(0.05);
space.sky.set({ color: 0, ambient: { intensity: 0.6 } });
document.body.classList.add('dark');
} else {
space.platform.set({ light: 0.08 });
space.platform.setFont({rulerColor:'#333333'});
space.platform.setGrid(gridMajor, gridMinor, 0x999999, 0xcccccc);
space.platform.setGrid(gridMajor, gridMinor, scheme.grid.major, scheme.grid.minor);
space.platform.opacity(0.2);
space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } });
document.body.classList.remove('dark');
@ -319,16 +330,18 @@ function update_selected() {
let b = $(`sel-ext-${i}`);
if (b) b.classList.remove('pop-sel');
}
const scheme = getColorScheme();
selection.for_widgets(w => {
w.setColor(COLOR.selected);
w.setColor(scheme.widget.selected);
let ext = api.widgets.annotate(w.id).extruder || 0;
let b = $(`sel-ext-${ext}`);
if (b) b.classList.add('pop-sel');
w.saveState();
}, true);
} else {
const scheme = getColorScheme();
selection.for_widgets(w => {
w.setColor(COLOR.selected);
w.setColor(scheme.widget.selected);
}, true);
}
}
@ -374,7 +387,8 @@ function select(widget, shift, recurse = true) {
}
selection.add(widget);
event.emit('widget.select', widget);
widget.setColor(COLOR.selected);
const scheme = getColorScheme();
widget.setColor(scheme.widget.selected);
$(`ws-${widget.id}`)?.classList.add('selected');
selection.update_info();
}
@ -420,7 +434,8 @@ function deselect(widget, recurse = true) {
}
$(`ws-${widget.id}`)?.classList.remove('selected');
widget.setColor(COLOR.deselected);
const scheme = getColorScheme();
widget.setColor(scheme.widget.deselected);
platform.update_selected();
selection.update_info();
space.update();
@ -753,8 +768,9 @@ function changed() {
onclick() {
let dis = w.meta.disabled = !w.meta.disabled;
let sel = api.selection.contains(w);
const scheme = getColorScheme();
api.uc.setClass($(`w-en-${w.id}`), "disabled", dis);
w.setColor(sel ? COLOR.selected : COLOR.deselected, null, false);
w.setColor(sel ? scheme.widget.selected : scheme.widget.deselected, null, false);
}
},
[ h.i({ class:"fas fa-ban" }) ]

View file

@ -0,0 +1,215 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
/**
* VisualStateManager: Manages 3-layer visual state hierarchy for widgets
*
* Layer hierarchy (bottom to top):
* 1. Base Layer: User persistent preferences (edges, wireframe, opacity)
* 2. Mode Layer: Mode+view specific defaults (FDM/ARRANGE vs CAM/PREVIEW)
* 3. Operation Layer: Temporary context overrides (paint, selection)
*
* Context types:
* - 'paint': OVERRIDE context (FDM support painting, completely replaces material)
* - 'select': TEMPORARY context (selection highlighting)
* - null: Normal state changes following hierarchy
*/
class VisualStateManager {
constructor(widget, scheme, api) {
this.widget = widget;
this.scheme = scheme;
this.api = api;
// State stack (base → mode → operation)
this.stack = [];
// Active operation states
this.operations = {}; // { context: state }
}
/**
* Load user preferences from localStorage (base layer)
*/
loadPreferences() {
const edges = this.api.local.getBoolean('model.edges');
const wireframe = this.api.local.getBoolean('model.wireframe');
const wireframeColor = this.api.local.getInt('model.wireframe.color') || 0;
const wireframeOpacity = this.api.local.getFloat('model.wireframe.opacity');
this.baseState = {
edges: edges || false,
wireframe: wireframe || false,
wireframeColor: wireframeColor,
wireframeOpacity: wireframeOpacity !== null ? wireframeOpacity : this.scheme.wireframe.opacity,
opacity: 1.0
};
return this.baseState;
}
/**
* Save user preferences to localStorage
*/
savePreferences() {
if (!this.baseState) return;
this.api.local.set('model.edges', this.baseState.edges);
this.api.local.set('model.wireframe', this.baseState.wireframe);
if (this.baseState.wireframeColor !== undefined) {
this.api.local.set('model.wireframe.color', this.baseState.wireframeColor);
}
if (this.baseState.wireframeOpacity !== undefined) {
this.api.local.set('model.wireframe.opacity', this.baseState.wireframeOpacity);
}
}
/**
* Apply mode-specific defaults (mode layer)
* @param {number} mode - Mode ID
* @param {number} viewMode - View mode ID (ARRANGE=1, SLICE=2, PREVIEW=3, ANIMATE=4)
*/
applyModeState(mode, viewMode) {
const viewName = this._viewModeToName(viewMode);
const viewScheme = this.scheme.views[viewName];
this.modeState = {
mode,
viewMode,
opacity: viewScheme?.model_opacity ?? 1.0,
previewOpacity: viewScheme?.preview_opacity,
slicingOpacity: viewScheme?.slicing_opacity,
slicedOpacity: viewScheme?.sliced_opacity
};
return this.modeState;
}
/**
* Push a temporary operation state (operation layer)
* @param {string} context - Operation context ('paint', 'select', etc)
* @param {object} state - State to push
* @param {function} state.restoreCallback - Optional callback to restore state on pop
*/
pushState(context, state) {
if (!context) {
console.warn('pushState called without context');
return;
}
this.operations[context] = state;
this.stack.push({ layer: 'operation', context, state });
return state;
}
/**
* Pop a temporary operation state
* @param {string} context - Operation context to pop
*/
popState(context) {
if (!context) {
console.warn('popState called without context');
return;
}
const operation = this.operations[context];
if (!operation) {
console.warn(`No operation found for context: ${context}`);
return;
}
// Call restore callback if provided
if (operation.restoreCallback) {
operation.restoreCallback();
}
// Remove from operations and stack
delete this.operations[context];
this.stack = this.stack.filter(s => s.context !== context);
}
/**
* Get the current effective state (topmost in hierarchy)
*/
getCurrentState() {
if (this.stack.length > 0) {
return this.stack[this.stack.length - 1].state;
}
if (this.modeState) {
return this.modeState;
}
return this.baseState;
}
/**
* Check if an operation context is currently active
* @param {string} context - Operation context to check
* @returns {boolean}
*/
isOperationActive(context) {
return context in this.operations;
}
/**
* Get all active operation contexts
* @returns {Array<string>}
*/
getActiveContexts() {
return Object.keys(this.operations);
}
/**
* Clear all operation states (e.g., on mode switch)
*/
clearOperations() {
// Call restore callbacks for all active operations
for (const context in this.operations) {
const operation = this.operations[context];
if (operation.restoreCallback) {
operation.restoreCallback();
}
}
this.operations = {};
this.stack = this.stack.filter(s => s.layer !== 'operation');
}
/**
* Get visual state snapshot for undo/redo
*/
getVisualState() {
return {
edges: this.widget.outline ? true : false,
wires: this.widget.wire ? true : false,
opacity: this.widget.getMaterial()?.opacity ?? 1.0
};
}
/**
* Restore visual state from snapshot
* @param {object} state - State snapshot
*/
setVisualState({ edges, wires, opacity }) {
this.widget.cache.vizstate = this.getVisualState();
this.widget.setEdges(edges ?? false);
this.widget.setWireframe(wires ?? false);
if (opacity !== undefined) {
this.widget.setOpacity(opacity);
}
}
/**
* Convert view mode ID to name
* @private
*/
_viewModeToName(viewMode) {
const views = {
1: 'ARRANGE',
2: 'SLICE',
3: 'PREVIEW',
4: 'ANIMATE'
};
return views[viewMode] || 'ARRANGE';
}
}
export { VisualStateManager };

View file

@ -3,12 +3,22 @@
import { api } from './api.js';
import { base } from '../../geo/base.js';
import { consts } from '../core/consts.js';
import { colorSchemeRegistry } from './color/schemes.js';
import { settings } from './conf/manager.js';
import { space } from '../../moto/space.js';
import STACKS from './stacks.js';
const { COLOR, VIEWS } = consts;
const { VIEWS } = consts;
/**
* Get current color scheme
*/
function getColorScheme() {
const mode = api.mode.get_id();
const theme = api.space.is_dark() ? 'dark' : 'light';
return colorSchemeRegistry.getScheme(mode, theme);
}
/**
* Constrain value to min/max bounds.
@ -182,7 +192,8 @@ function hideSlices() {
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 opacity = saved !== null ? saved : COLOR.model_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));

View file

@ -3,6 +3,8 @@
import { Widget as WidgetCore } from '../core/widget.js';
import { util } from '../../geo/base.js';
import { avgc } from '../core/utils.js';
import { colorSchemeRegistry } from './color/schemes.js';
import { VisualStateManager } from './state/visual.js';
const solid_opacity = 1.0;
@ -14,6 +16,59 @@ function index() { return self.kiri_catalog.index }
* Adds rendering, persistence, and visual state management
*/
class Widget extends WidgetCore {
constructor(id, group) {
super(id, group);
// Visual state manager will be initialized when api is available
this.visualStateManager = null;
}
/**
* Initialize visual state manager (called when api is available)
* @private
*/
_initVisualStateManager() {
if (this.visualStateManager || !this.api) {
return;
}
const mode = this.api.mode.get_id();
const theme = this.api.space.is_dark() ? 'dark' : 'light';
const scheme = colorSchemeRegistry.getScheme(mode, theme);
this.visualStateManager = new VisualStateManager(this, scheme, this.api);
this.visualStateManager.loadPreferences();
}
/**
* Get current color scheme
* @private
*/
_getScheme() {
if (!this.api) return null;
const mode = this.api.mode.get_id();
const theme = this.api.space.is_dark() ? 'dark' : 'light';
return colorSchemeRegistry.getScheme(mode, theme);
}
/**
* Push a temporary visual state (for operations like paint)
*/
pushVisualState(context, state) {
this._initVisualStateManager();
if (this.visualStateManager) {
return this.visualStateManager.pushState(context, state);
}
}
/**
* Pop a temporary visual state
*/
popVisualState(context) {
if (this.visualStateManager) {
this.visualStateManager.popState(context);
}
}
/**
* Save widget geometry to catalog
*/
@ -144,25 +199,57 @@ class Widget extends WidgetCore {
// missing api features in engine mode
return;
}
let mesh = this.mesh;
const mesh = this.mesh;
// Handle toggle
if (set && set.toggle) {
set = this.outline ? false : true;
}
// Remove outline (but keep cache)
if (this.outline) {
mesh.remove(this.outline);
this.outline = null;
}
if (set) {
let dark = this.api.space.is_dark();
let cam = this.api.mode.is_cam();
let color = dark ? 0x444444 : 0x888888;
let angle = this.api.conf.get().controller.edgeangle || 20;
let edges = new THREE.EdgesGeometry(mesh.geometry, angle);
let material = new THREE.LineBasicMaterial({ color });
this.outline = new THREE.LineSegments(edges, material);
this.outline.renderOrder = -20;
mesh.add(this.outline);
if (!set) {
return; // Just hide, keep cache
}
// Resolve current angle and color
const scheme = this._getScheme();
const angle = scheme?.edges?.angle ?? this.api.conf.get().controller.edgeangle ?? 20;
const color = scheme?.edges?.color ?? 0x888888;
// Initialize cache if needed
if (!this.cache.edges) {
this.cache.edges = { geometry: null, angle: null, version: 0 };
}
const cache = this.cache.edges;
let edgesGeometry;
// Check if we can reuse cached geometry
if (cache.geometry &&
cache.angle === angle &&
cache.version === this.geomVersion) {
// Reuse cached geometry
edgesGeometry = cache.geometry;
} else {
// Regenerate edges
edgesGeometry = new THREE.EdgesGeometry(mesh.geometry, angle);
// Update cache
cache.geometry = edgesGeometry;
cache.angle = angle;
cache.version = this.geomVersion;
}
// Create outline with cached/new geometry
const material = new THREE.LineBasicMaterial({ color });
this.outline = new THREE.LineSegments(edgesGeometry, material);
this.outline.renderOrder = -20;
mesh.add(this.outline);
}
/**
@ -181,14 +268,16 @@ class Widget extends WidgetCore {
this.wire = null;
}
if (set) {
let dark = this.api.space.is_dark();
let mat = new THREE.MeshBasicMaterial({
const scheme = this._getScheme();
const wireColor = scheme?.wireframe?.color ?? 0xaaaaaa;
const wireOpacity = scheme?.wireframe?.opacity ?? 0.5;
const mat = new THREE.MeshBasicMaterial({
wireframe: true,
color: dark ? 0xaaaaaa : 0,
opacity: 0.5,
color: wireColor,
opacity: wireOpacity,
transparent: true
})
let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat);
});
const wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat);
mesh.add(wire);
}
if (this.api.view.is_arrange()) {

View file

@ -1,21 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const COLOR = {
wireframe: 0x444444,
wireframe_opacity: 0.25,
selected: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ],
deselected: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ],
model_opacity: 1.0,
preview_opacity: 0.0,
slicing: 0xffaaaa,
slicing_opacity: 0.5,
sliced_opacity: 0.0,
cam_sliced_opacity: 0.2,
cam_preview: 0xdddddd,
cam_preview_opacity: 0.2,
cam_preview_dark: 0x888888,
cam_preview_opacity_dark: 0.2,
};
// COLOR table has been migrated to src/kiri/app/color/ scheme system
const LISTS = {
shell: [
@ -185,7 +170,6 @@ const LAST = 'kiri-last';
const SEED = 'kiri-seed';
export let consts = {
COLOR,
LAST,
LISTS,
MODES,
@ -196,4 +180,4 @@ export let consts = {
beltfact: Math.cos(Math.PI / 4)
};
export { COLOR, LAST, LISTS, MODES, VIEWS, PATHS, PMODES, SEED };
export { LAST, LISTS, MODES, VIEWS, PATHS, PMODES, SEED };

View file

@ -49,6 +49,8 @@ class Widget {
this.boundingBoxNeedsUpdate = true
// cache shadow geo
this.cache = {};
// geometry version for edge cache invalidation
this.geomVersion = 0;
this.stats = {
slice_time: 0,
load_time: 0,
@ -193,6 +195,7 @@ class Widget {
this.clearShadows();
if (reason !== 'axis') {
this.cache.geo = undefined;
this.geomVersion++; // Increment version to invalidate edge cache
}
if (this.mesh && this.mesh.geometry) {
// this fixes ray intersections after the mesh is modified

View file

@ -1,6 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../app/api.js';
import { colorSchemeRegistry } from '../../app/color/schemes.js';
import { env, isDark } from './init-ui.js';
import { addbox, delbox, clearboxes } from '../../core/boxes.js';
import { traceDone } from './cl-trace.js';
@ -225,9 +226,15 @@ export function tabClear() {
}
function boxColor() {
return isDark() ? 0x00ddff : 0x0000dd;
const mode = 3; // CAM mode ID
const theme = isDark() ? 'dark' : 'light';
const scheme = colorSchemeRegistry.getScheme(mode, theme);
return scheme.operations?.tabs?.color ?? (isDark() ? 0x00ddff : 0x0000dd);
}
function boxOpacity() {
return isDark() ? 0.75 : 0.6;
const mode = 3; // CAM mode ID
const theme = isDark() ? 'dark' : 'light';
const scheme = colorSchemeRegistry.getScheme(mode, theme);
return scheme.operations?.tabs?.opacity ?? (isDark() ? 0.75 : 0.6);
}

View file

@ -3,6 +3,7 @@
import { newPoint } from '../../../geo/point.js';
import { $ } from '../../../moto/webui.js';
import { api } from '../../app/api.js';
import { colorSchemeRegistry } from '../../app/color/schemes.js';
import { conf } from '../../app/conf/defaults.js';
import { addPaintOverlayTexture, updatePaintOverlay } from '../../app/paint.js';
@ -271,14 +272,29 @@ function supportStart({ remove } = { remove: false }) {
return [ down.widget.mesh ];
// return api.widgets.meshes();
};
// Get paint color from scheme
const mode = api.mode.get_id();
const theme = api.space.is_dark() ? 'dark' : 'light';
const scheme = colorSchemeRegistry.getScheme(mode, theme);
const paintColor = scheme.operations?.paint?.overlay ?? 0x4488ff;
api.widgets.each(w => {
// Use pushVisualState to track the paint operation
w.pushVisualState('paint', {
material: w.mesh.material[0],
restoreCallback: () => {
w.mesh.material[0] = w.cache.mat;
delete w.cache.mat;
}
});
let mat = w.cache.mat = w.mesh.material[0];
mat = w.mesh.material[0] = mat.clone();
mat.needsUpdate = true;
if (!w.anno.paint) {
w.anno.paint = [];
}
addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(0x4488ff));
addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(paintColor));
})
api.space.update();
}
@ -321,8 +337,8 @@ function supportDone() {
api.feature.on_mouse_drag = undefined;
api.hide.alert(alert);
api.widgets.each(w => {
w.mesh.material[0] = w.cache.mat;
delete w.cache.mat;
// Use popVisualState to restore original material
w.popVisualState('paint');
});
}