refactor color and opacity/edge management
This commit is contained in:
parent
a13bb79f4b
commit
9900161dcd
17 changed files with 795 additions and 64 deletions
|
|
@ -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,
|
||||
|
|
|
|||
69
src/kiri/app/color/base.js
Normal file
69
src/kiri/app/color/base.js
Normal 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
58
src/kiri/app/color/cam.js
Normal 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
42
src/kiri/app/color/fdm.js
Normal 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 };
|
||||
28
src/kiri/app/color/laser.js
Normal file
28
src/kiri/app/color/laser.js
Normal 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 };
|
||||
145
src/kiri/app/color/schemes.js
Normal file
145
src/kiri/app/color/schemes.js
Normal 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
23
src/kiri/app/color/sla.js
Normal 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 };
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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" }) ]
|
||||
|
|
|
|||
215
src/kiri/app/state/visual.js
Normal file
215
src/kiri/app/state/visual.js
Normal 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 };
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue