mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-15 21:05:22 +01:00
implement generic boolean operation
This commit is contained in:
parent
7b01e228ff
commit
2a2b221f5d
21 changed files with 228 additions and 60 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import {CombineStream} from './combine';
|
||||
import {StateStream} from './state';
|
||||
import {DistinctStateStream, StateStream} from './state';
|
||||
import {Emitter} from './emitter';
|
||||
import {ExternalStateStream} from './external';
|
||||
import {MergeStream} from './merge';
|
||||
|
|
@ -24,6 +24,10 @@ export function state(initialValue) {
|
|||
return new StateStream(initialValue);
|
||||
}
|
||||
|
||||
export function distinctState(initialValue) {
|
||||
return new DistinctStateStream(initialValue);
|
||||
}
|
||||
|
||||
export function externalState(get, set) {
|
||||
return new ExternalStateStream(get, set);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,3 +35,12 @@ export class StateStream extends Emitter {
|
|||
}
|
||||
}
|
||||
|
||||
export class DistinctStateStream extends StateStream {
|
||||
|
||||
next(v) {
|
||||
if (this._value === v) {
|
||||
return;
|
||||
}
|
||||
super.next(v);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as Operations from '../craft/operations'
|
||||
import * as ActionHelpers from './actionHelpers'
|
||||
import * as Operations from '../craft/operations';
|
||||
import * as ActionHelpers from './actionHelpers';
|
||||
|
||||
// L E G A C Y
|
||||
const OPERATION_ACTIONS = [
|
||||
{
|
||||
id: 'SHELL',
|
||||
|
|
@ -9,33 +10,6 @@ const OPERATION_ACTIONS = [
|
|||
},
|
||||
...requiresFaceSelection(1)
|
||||
},
|
||||
{
|
||||
id: 'SPHERE',
|
||||
appearance: {
|
||||
info: 'creates new object sphere'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'INTERSECTION',
|
||||
appearance: {
|
||||
info: 'intersection operation on two solids',
|
||||
},
|
||||
...requiresSolidSelection(2)
|
||||
},
|
||||
{
|
||||
id: 'DIFFERENCE',
|
||||
appearance: {
|
||||
info: 'difference operation on two solids',
|
||||
},
|
||||
...requiresSolidSelection(2)
|
||||
},
|
||||
{
|
||||
id: 'UNION',
|
||||
appearance: {
|
||||
info: 'union operation on two solids',
|
||||
},
|
||||
...requiresSolidSelection(2)
|
||||
},
|
||||
{
|
||||
id: 'IMPORT_STL',
|
||||
appearance: {
|
||||
|
|
|
|||
12
web/app/cad/craft/boolean/BooleanWizard.jsx
Normal file
12
web/app/cad/craft/boolean/BooleanWizard.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import {Group} from '../wizard/components/form/Form';
|
||||
import BooleanChoice from '../wizard/components/form/BooleanChioce';
|
||||
import SingleEntity from '../wizard/components/form/SingleEntity';
|
||||
|
||||
export default function BooleanWizard() {
|
||||
return <Group>
|
||||
<SingleEntity name='operandA' label='operand A' entity='shell' selectionIndex={0} />
|
||||
<SingleEntity name='operandB' label='operand B' entity='shell' selectionIndex={1} />
|
||||
<BooleanChoice name='type' strict/>
|
||||
</Group>;
|
||||
}
|
||||
15
web/app/cad/craft/boolean/booleanOpSchema.js
Normal file
15
web/app/cad/craft/boolean/booleanOpSchema.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export default defaultValue => ({
|
||||
operandA: {
|
||||
type: 'shell',
|
||||
defaultValue: {type: 'selection'}
|
||||
},
|
||||
operandB: {
|
||||
type: 'shell',
|
||||
defaultValue: {type: 'selection'}
|
||||
},
|
||||
type: {
|
||||
type: 'enum',
|
||||
values: ['INTERSECT', 'SUBTRACT', 'UNION'],
|
||||
defaultValue
|
||||
}
|
||||
})
|
||||
52
web/app/cad/craft/boolean/booleanOperation.js
Normal file
52
web/app/cad/craft/boolean/booleanOperation.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import schema from './booleanOpSchema';
|
||||
import BooleanWizard from './BooleanWizard';
|
||||
|
||||
function run(params, services) {
|
||||
return services.craftEngine.boolean({
|
||||
type: params.type,
|
||||
operandsA: [services.cadRegistry.findShell(params.operandA)],
|
||||
operandsB: [services.cadRegistry.findShell(params.operandB)]
|
||||
});
|
||||
}
|
||||
|
||||
const paramsInfo = ({operandA, operandB}) => `(${operandA}, ${operandB})`;
|
||||
|
||||
const selectionMode = {
|
||||
shell: true
|
||||
};
|
||||
|
||||
export const intersectionOperation = {
|
||||
id: 'INTERSECTION',
|
||||
label: 'intersection',
|
||||
icon: 'img/cad/intersection',
|
||||
info: 'intersection operation on two shells',
|
||||
paramsInfo,
|
||||
form: BooleanWizard,
|
||||
schema: schema('INTERSECTION'),
|
||||
run,
|
||||
selectionMode
|
||||
};
|
||||
|
||||
export const subtractOperation = {
|
||||
id: 'SUBTRACT',
|
||||
label: 'subtract',
|
||||
icon: 'img/cad/subtract',
|
||||
info: 'subtract operation on two shells',
|
||||
paramsInfo,
|
||||
form: BooleanWizard,
|
||||
schema: schema('SUBTRACT'),
|
||||
run,
|
||||
selectionMode
|
||||
};
|
||||
|
||||
export const unionOperation = {
|
||||
id: 'UNION',
|
||||
label: 'union',
|
||||
icon: 'img/cad/union',
|
||||
info: 'union operation on two shells',
|
||||
paramsInfo,
|
||||
form: BooleanWizard,
|
||||
schema: schema('UNION'),
|
||||
run,
|
||||
selectionMode
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {DATUM, EDGE, FACE, SKETCH_OBJECT} from '../scene/entites';
|
||||
import {DATUM, EDGE, FACE, SHELL, SKETCH_OBJECT} from '../scene/entites';
|
||||
import {MShell} from '../model/mshell';
|
||||
|
||||
|
||||
|
|
@ -14,7 +14,17 @@ export function activate({streams, services}) {
|
|||
function getAllShells() {
|
||||
return streams.cadRegistry.shells.value;
|
||||
}
|
||||
|
||||
|
||||
function findShell(shellId) {
|
||||
let shells = getAllShells();
|
||||
for (let shell of shells) {
|
||||
if (shell.id === shellId) {
|
||||
return shell;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findFace(faceId) {
|
||||
let shells = getAllShells();
|
||||
for (let shell of shells) {
|
||||
|
|
@ -55,6 +65,7 @@ export function activate({streams, services}) {
|
|||
function findEntity(entity, id) {
|
||||
switch (entity) {
|
||||
case FACE: return findFace(id);
|
||||
case SHELL: return findShell(id);
|
||||
case EDGE: return findEdge(id);
|
||||
case SKETCH_OBJECT: return findSketchObject(id);
|
||||
case DATUM: return findDatum(id);
|
||||
|
|
@ -63,7 +74,7 @@ export function activate({streams, services}) {
|
|||
}
|
||||
|
||||
services.cadRegistry = {
|
||||
getAllShells, findFace, findEdge, findSketchObject, findEntity, findDatum,
|
||||
getAllShells, findShell, findFace, findEdge, findSketchObject, findEntity, findDatum,
|
||||
get modelIndex() {
|
||||
return streams.cadRegistry.modelIndex.value;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
|
||||
export default {
|
||||
|
||||
createBox: params => notImplemented,
|
||||
createSphere: params => notImplemented,
|
||||
createCone: params => notImplemented,
|
||||
createCylinder: params => notImplemented,
|
||||
createTorus: params => notImplemented,
|
||||
|
||||
boolean: params => notImplemented,
|
||||
}
|
||||
|
||||
function notImplemented() {
|
||||
|
|
|
|||
|
|
@ -23,12 +23,15 @@ export function activate(ctx) {
|
|||
loadWasm(ctx);
|
||||
|
||||
ctx.services.operation.handlers.push(operationHandler);
|
||||
function shellsToPointers(shells) {
|
||||
return shells.filter(managedByE0).map(m => m.brepShell.data.externals.ptr);
|
||||
}
|
||||
function booleanBasedOperation(engineParams, params, impl) {
|
||||
engineParams.deflection = DEFLECTION;
|
||||
if (params.boolean && BOOLEAN_TYPES[params.boolean.type] > 0) {
|
||||
engineParams.boolean = {
|
||||
type: BOOLEAN_TYPES[params.boolean.type],
|
||||
operands: params.boolean.operands.filter(managedByE0).map(m => m.brepShell.data.externals.ptr),
|
||||
operands: shellsToPointers(params.boolean.operands),
|
||||
tolerance: TOLERANCE,
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +86,21 @@ export function activate(ctx) {
|
|||
csys: writeCsys(params.csys),
|
||||
r: params.radius,
|
||||
}, params, Module._SPI_sphere);
|
||||
},
|
||||
boolean: function({type, operandsA, operandsB}) {
|
||||
let engineParams = {
|
||||
type: BOOLEAN_TYPES[type],
|
||||
operandsA: shellsToPointers(operandsA),
|
||||
operandsB: shellsToPointers(operandsB),
|
||||
tolerance: TOLERANCE,
|
||||
deflection: DEFLECTION,
|
||||
};
|
||||
let data = callEngine(engineParams, Module._SPI_boolean);
|
||||
let consumed = [...operandsA, ...operandsB];
|
||||
return {
|
||||
consumed,
|
||||
created: [readShellData(data.result, consumed, operandsA[0].csys)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import React from 'react';
|
|||
import {ComboBoxOption} from 'ui/components/controls/ComboBoxControl';
|
||||
import {ComboBoxField} from './Fields';
|
||||
|
||||
export default function BooleanChoice(props) {
|
||||
export default function BooleanChoice({strict, ...props}) {
|
||||
return <ComboBoxField {...props}>
|
||||
<ComboBoxOption value=''>{'<none>'}</ComboBoxOption>
|
||||
{!strict && <ComboBoxOption value=''>{'<none>'}</ComboBoxOption>}
|
||||
<ComboBoxOption value={'INTERSECT'}>intersect</ComboBoxOption>
|
||||
<ComboBoxOption value={'SUBTRACT'}>subtract</ComboBoxOption>
|
||||
<ComboBoxOption value={'UNION'}>union</ComboBoxOption>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import Fa from 'ui/components/Fa';
|
|||
import Button from 'ui/components/controls/Button';
|
||||
import {attachToForm} from './Form';
|
||||
import {camelCaseSplitToStr} from 'gems/camelCaseSplit';
|
||||
import NumberControl from '../../../../../../../modules/ui/components/controls/NumberControl';
|
||||
|
||||
@attachToForm
|
||||
@mapContext(({streams, services}) => ({
|
||||
|
|
@ -16,12 +17,14 @@ import {camelCaseSplitToStr} from 'gems/camelCaseSplit';
|
|||
export default class SingleEntity extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
let {streams, entity, onChange, value, findEntity} = this.props;
|
||||
let {streams, entity, onChange, value, selectionIndex, findEntity} = this.props;
|
||||
let selection$ = streams.selection[entity];
|
||||
if (findEntity(entity, value)) {
|
||||
selection$.next([value]);
|
||||
if (selectionIndex === 0) {
|
||||
selection$.next([value]);
|
||||
}
|
||||
}
|
||||
this.detacher = selection$.attach(selection => onChange(selection[0]));
|
||||
this.detacher = selection$.attach(selection => onChange(selection[selectionIndex]));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
@ -34,8 +37,8 @@ export default class SingleEntity extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
let {name, label, streams, entity} = this.props;
|
||||
let selection = streams.selection[entity].value[0];
|
||||
let {name, label, streams, selectionIndex, entity} = this.props;
|
||||
let selection = streams.selection[entity].value[selectionIndex];
|
||||
return <Field>
|
||||
<Label>{label||camelCaseSplitToStr(name)}:</Label>
|
||||
<div>{selection ?
|
||||
|
|
@ -44,3 +47,7 @@ export default class SingleEntity extends React.Component {
|
|||
</Field>;
|
||||
}
|
||||
}
|
||||
|
||||
SingleEntity.defaultProps = {
|
||||
selectionIndex: 0
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
export function activate(ctx) {
|
||||
ctx.streams.wizard.workingRequest.attach(({type}) => {
|
||||
if (type) {
|
||||
let operation = ctx.services.operation.get(type);
|
||||
if (operation.selectionMode) {
|
||||
ctx.services.pickControl.setSelectionMode(operation.selectionMode);
|
||||
}
|
||||
} else {
|
||||
ctx.services.pickControl.switchToDefaultSelectionMode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import * as UiPlugin from '../dom/uiPlugin';
|
|||
import * as MenuPlugin from '../dom/menu/menuPlugin';
|
||||
import * as KeyboardPlugin from '../keyboard/keyboardPlugin';
|
||||
import * as WizardPlugin from '../craft/wizard/wizardPlugin';
|
||||
import * as WizardSelectionModeSwitcherPlugin from '../craft/wizard/wizardSelectionModeSwitcherPlugin';
|
||||
import * as PreviewPlugin from '../preview/previewPlugin';
|
||||
import * as OperationPlugin from '../craft/operationPlugin';
|
||||
import * as ExtensionsPlugin from '../craft/extensionsPlugin';
|
||||
|
|
@ -63,7 +64,8 @@ export default function startApplication(callback) {
|
|||
SelectionMarkerPlugin,
|
||||
SketcherPlugin,
|
||||
...applicationPlugins,
|
||||
ViewSyncPlugin
|
||||
ViewSyncPlugin,
|
||||
WizardSelectionModeSwitcherPlugin
|
||||
];
|
||||
|
||||
let allPlugins = [...preUIPlugins, ...plugins];
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ export default [
|
|||
label: 'bool',
|
||||
cssIcons: ['pie-chart'],
|
||||
info: 'set of available boolean operations',
|
||||
actions: ['INTERSECTION', 'DIFFERENCE', 'UNION']
|
||||
actions: ['INTERSECTION', 'SUBTRACT', 'UNION']
|
||||
},
|
||||
{
|
||||
id: 'main',
|
||||
label: 'start',
|
||||
cssIcons: ['rocket'],
|
||||
info: 'common set of actions',
|
||||
actions: ['EXTRUDE', 'CUT', 'SHELL', '-', 'INTERSECTION', 'DIFFERENCE', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-',
|
||||
actions: ['EXTRUDE', 'CUT', 'SHELL', '-', 'INTERSECTION', 'SUBTRACT', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-',
|
||||
'EditFace', '-', 'DeselectAll', 'RefreshSketches']
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import sphereOperation from '../craft/primitives/sphere/sphereOperation';
|
|||
import cylinderOperation from '../craft/primitives/cylinder/cylinderOperation';
|
||||
import torusOperation from '../craft/primitives/torus/torusOperation';
|
||||
import coneOperation from '../craft/primitives/cone/coneOperation';
|
||||
import {intersectionOperation, subtractOperation, unionOperation} from '../craft/boolean/booleanOperation';
|
||||
|
||||
export function activate({services}) {
|
||||
services.operation.registerOperations([
|
||||
|
|
@ -28,6 +29,9 @@ export function activate({services}) {
|
|||
sphereOperation,
|
||||
cylinderOperation,
|
||||
torusOperation,
|
||||
coneOperation
|
||||
coneOperation,
|
||||
intersectionOperation,
|
||||
subtractOperation,
|
||||
unionOperation
|
||||
])
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ export function activate({services, streams}) {
|
|||
['ShowSketches', {label: 'sketches'}], ['DeselectAll', {label: null}], ['ToggleCameraMode', {label: null}]
|
||||
];
|
||||
|
||||
streams.ui.toolbars.headsUp.value = ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE', 'FILLET', 'INTERSECTION', 'DIFFERENCE', 'UNION'];
|
||||
streams.ui.toolbars.headsUp.value = ['PLANE', 'EditFace', 'EXTRUDE', 'CUT', 'REVOLVE', 'FILLET', 'INTERSECTION', 'SUBTRACT', 'UNION'];
|
||||
streams.ui.toolbars.auxiliary.value = ['Save', 'StlExport'];
|
||||
|
||||
services.action.registerActions(CoreActions);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import * as mask from 'gems/mask'
|
||||
import {getAttribute, setAttribute} from 'scene/objectData';
|
||||
import {FACE, EDGE, SKETCH_OBJECT, DATUM} from '../entites';
|
||||
import {FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL} from '../entites';
|
||||
import {state} from 'lstream';
|
||||
import {distinctState} from '../../../../../modules/lstream';
|
||||
|
||||
export const PICK_KIND = {
|
||||
FACE: mask.type(1),
|
||||
|
|
@ -9,7 +10,16 @@ export const PICK_KIND = {
|
|||
EDGE: mask.type(3)
|
||||
};
|
||||
|
||||
const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM];
|
||||
const SELECTABLE_ENTITIES = [FACE, EDGE, SKETCH_OBJECT, DATUM, SHELL];
|
||||
|
||||
const DEFAULT_SELECTION_MODE = Object.freeze({
|
||||
shell: false,
|
||||
vertex: false,
|
||||
face: true,
|
||||
edge: true,
|
||||
sketchObject: true,
|
||||
datum: true
|
||||
});
|
||||
|
||||
export function activate(context) {
|
||||
const {services, streams} = context;
|
||||
|
|
@ -48,11 +58,18 @@ export function activate(context) {
|
|||
|
||||
function handlePick(event) {
|
||||
raycastObjects(event, PICK_KIND.FACE | PICK_KIND.SKETCH | PICK_KIND.EDGE, (view, kind) => {
|
||||
let selectionMode = streams.pickControl.selectionMode.value;
|
||||
let modelId = view.model.id;
|
||||
if (kind === PICK_KIND.FACE) {
|
||||
if (dispatchSelection(streams.selection.face, modelId, event)) {
|
||||
services.cadScene.showGlobalCsys(view.model.csys);
|
||||
return false;
|
||||
if (selectionMode.shell) {
|
||||
if (dispatchSelection(streams.selection.shell, view.model.shell.id, event)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (dispatchSelection(streams.selection.face, modelId, event)) {
|
||||
services.cadScene.showGlobalCsys(view.model.csys);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (kind === PICK_KIND.SKETCH) {
|
||||
if (dispatchSelection(streams.selection.sketchObject, modelId, event)) {
|
||||
|
|
@ -132,10 +149,35 @@ export function defineStreams({streams}) {
|
|||
SELECTABLE_ENTITIES.forEach(entity => {
|
||||
streams.selection[entity] = state([]);
|
||||
});
|
||||
streams.pickControl = {
|
||||
selectionMode: distinctState(DEFAULT_SELECTION_MODE)
|
||||
}
|
||||
}
|
||||
|
||||
function initStateAndServices({streams, services}) {
|
||||
|
||||
streams.pickControl.selectionMode.pairwise().attach(([prev, curr]) => {
|
||||
SELECTABLE_ENTITIES.forEach(entity => {
|
||||
if (prev[entity] !== curr[entity]) {
|
||||
streams.selection[entity].next([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function setSelectionMode(selectionMode) {
|
||||
streams.pickControl.selectionMode.next({
|
||||
...DEFAULT_SELECTION_MODE, ...selectionMode
|
||||
});
|
||||
}
|
||||
|
||||
function switchToDefaultSelectionMode() {
|
||||
streams.pickControl.selectionMode.next(DEFAULT_SELECTION_MODE);
|
||||
}
|
||||
|
||||
services.pickControl = {
|
||||
setSelectionMode, switchToDefaultSelectionMode
|
||||
};
|
||||
|
||||
services.selection = {};
|
||||
|
||||
SELECTABLE_ENTITIES.forEach(entity => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {EDGE, FACE, SKETCH_OBJECT} from '../entites';
|
||||
import {EDGE, FACE, SHELL, SKETCH_OBJECT} from '../entites';
|
||||
import {findDiff} from '../../../../../modules/gems/iterables';
|
||||
|
||||
export function activate({streams, services}) {
|
||||
|
|
@ -20,10 +20,8 @@ export function activate({streams, services}) {
|
|||
};
|
||||
|
||||
streams.selection.face.pairwise([]).attach(selectionSync(FACE));
|
||||
streams.selection.shell.pairwise([]).attach(selectionSync(SHELL));
|
||||
streams.selection.edge.pairwise([]).attach(selectionSync(EDGE));
|
||||
streams.selection.sketchObject.pairwise([]).attach(selectionSync(SKETCH_OBJECT));
|
||||
// new SelectionMarker(context, 0xFAFAD2, 0xFF0000, null);
|
||||
// new SketchSelectionMarker(context, createLineMaterial(0xFF0000, 6 / DPR));
|
||||
// new EdgeSelectionMarker(context, 0xFA8072);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {View} from './view';
|
|||
import * as SceneGraph from '../../../../../modules/scene/sceneGraph';
|
||||
import {setAttribute} from '../../../../../modules/scene/objectData';
|
||||
import {createSolidMaterial} from '../wrappers/sceneObject';
|
||||
import {FaceView} from './faceView';
|
||||
import {FaceView, SELECTION_COLOR} from './faceView';
|
||||
import {EdgeView} from './edgeView';
|
||||
import {SHELL} from '../entites';
|
||||
|
||||
|
|
@ -44,8 +44,16 @@ export class ShellView extends View {
|
|||
SceneGraph.addToGroup(this.edgeGroup, edgeView.rootGroup);
|
||||
this.edgeViews.push(edgeView);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mark(color) {
|
||||
this.faceViews.forEach(faceView => faceView.setColor(color || SELECTION_COLOR));
|
||||
}
|
||||
|
||||
withdraw(color) {
|
||||
this.faceViews.forEach(faceView => faceView.setColor(null));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (let faceView of this.faceViews) {
|
||||
faceView.dispose();
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Loading…
Reference in a new issue