区段拆分 && 区段道岔联动拖动

This commit is contained in:
Yuan 2023-07-03 18:45:18 +08:00
parent a3c079ba48
commit 2c219da3a2
10 changed files with 380 additions and 70 deletions

@ -1 +1 @@
Subproject commit 7e4eaed0cf06d68c75cb51c30329eff5fe4d1e3f Subproject commit 1f302648b5a71a82b798b77fe238c5fc6e3081b4

View File

@ -110,6 +110,8 @@ module.exports = configure(function (/* ctx */) {
// components: [], // components: [],
// directives: [], // directives: [],
autoImportComponentCase: 'combined',
// Quasar plugins // Quasar plugins
plugins: ['Notify', 'Dialog', 'Dark', 'AppFullscreen', 'Loading'], plugins: ['Notify', 'Dialog', 'Dark', 'AppFullscreen', 'Loading'],
}, },

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
import { useDialogPluginComponent } from 'quasar';
import { ref } from 'vue';
const num = ref(3);
const dir = ref('ltr');
const dirOptions = [
{ label: '从左至右', value: 'ltr' },
{ label: '从右至左', value: 'rtl' },
];
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef">
<QCard>
<QCardSection> <div class="text-h6">区段拆分</div> </QCardSection>
<QCardSection> <div>请选择要拆分的数量和方向</div> </QCardSection>
<QCardSection class="q-pt-none">
<QInput type="number" dense outlined v-model="num" :min="2" :max="20" />
</QCardSection>
<QCardSection>
<QSelect v-model="dir" dense :options="dirOptions" emitValue mapOptions>
</QSelect>
</QCardSection>
<QCardActions align="right" class="text-primary">
<QBtn flat label="取消" @click="onDialogCancel" v-close-popup />
<QBtn
flat
label="确认"
@click="onDialogOK({ num: Number(num), dir })"
v-close-popup
/>
</QCardActions>
</QCard>
</QDialog>
</template>
<style scoped></style>

View File

@ -31,16 +31,6 @@
> >
</template> </template>
</q-field> </q-field>
<q-input class="q-mt-lg" outlined v-model="splitNum" type="number">
<template #after>
<q-btn
:disable="sectionModel.data.sectionType !== SectionType.Physical"
color="primary"
@click="splitSection"
>拆分为逻辑区段</q-btn
>
</template>
</q-input>
</q-form> </q-form>
</template> </template>
@ -56,34 +46,6 @@ const drawStore = useDrawStore();
const sectionModel = shallowRef(new SectionData()); const sectionModel = shallowRef(new SectionData());
const splitNum = ref(3);
function splitSection() {
const sectionData = toRaw(sectionModel.value);
const section = toRaw(drawStore.selectedGraphic as Section);
const app = drawStore.getDrawApp();
const points = section.getSplitPoints(splitNum.value);
const childIds: string[] = [];
points.forEach((ps, i) => {
const data = new SectionData();
data.points = ps.map((p) => new graphicData.Point({ x: p.x, y: p.y }));
data.id = app.drawAssistants
.find((as) => as.name === Section.name)!
.nextId();
data.code = `${sectionData.code}-${'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.charAt(
i % 26
)}`;
data.sectionType = SectionType.Logic;
const section = app.graphicTemplateMap.get(Section.name)!.new();
section?.loadData(data);
app.addGraphics(section);
childIds.push(data.id);
});
sectionData.children = childIds;
app.updateGraphicAndRecord(section, sectionData);
}
const sectionRelations = computed(() => { const sectionRelations = computed(() => {
const section = drawStore.selectedGraphic as Section; const section = drawStore.selectedGraphic as Section;

View File

@ -17,6 +17,7 @@ import {
} from '../CommonGraphics'; } from '../CommonGraphics';
import { Turnout } from '../turnout/Turnout'; import { Turnout } from '../turnout/Turnout';
import { SectionData } from 'src/drawApp/graphics/SectionInteraction'; import { SectionData } from 'src/drawApp/graphics/SectionInteraction';
import Vector2 from 'src/jl-graphic/math/Vector2';
export enum SectionType { export enum SectionType {
Physical = 0, Physical = 0,
@ -134,13 +135,49 @@ export class Section extends JlGraphic implements ILineGraphic {
/** 获取拆分逻辑区段数据 */ /** 获取拆分逻辑区段数据 */
getSplitPoints(count: number): IPointData[][] { getSplitPoints(count: number): IPointData[][] {
if (this.datas.points.length !== 2) { if (this.datas.points.length !== 2) {
throw Error('多段分割待实现'); // throw Error('多段分割待实现');
let totalLen = 0;
const lengths: number[] = [];
for (let i = 1; i < this.datas.points.length; i++) {
const { x: x1, y: y1 } = this.datas.points[i - 1],
{ x: x2, y: y2 } = this.datas.points[i];
const len = new Vector2([x2 - x1, y2 - y1]).length();
totalLen += len;
lengths.push(len);
}
const counts = lengths.map((length) =>
Math.round((count * length) / totalLen)
);
if (counts.reduce((p, c) => p + c, 0) !== count) {
const intersection = counts.reduce((p, c) => p + c, 0) - count;
let maxCountIndex = 0,
maxCount = 0;
counts.forEach((c, i) => {
if (c > maxCount) {
maxCount = c;
maxCountIndex = i;
}
});
counts[maxCountIndex] + intersection;
}
return counts
.map((count, i) => {
return splitLineEvenly(
this.localToCanvasPoint(this.datas.points[i]),
this.localToCanvasPoint(this.datas.points[i + 1]),
count
);
})
.flat();
} else {
return splitLineEvenly(
this.localToCanvasPoint(this.datas.points[0]),
this.localToCanvasPoint(
this.datas.points[this.datas.points.length - 1]
),
count
);
} }
return splitLineEvenly(
this.localToCanvasPoint(this.datas.points[0]),
this.localToCanvasPoint(this.datas.points[this.datas.points.length - 1]),
count
);
} }
buildRelation() { buildRelation() {

View File

@ -4,6 +4,7 @@ import {
GraphicApp, GraphicApp,
GraphicDrawAssistant, GraphicDrawAssistant,
GraphicInteractionPlugin, GraphicInteractionPlugin,
GraphicRelation,
GraphicTransform, GraphicTransform,
GraphicTransformEvent, GraphicTransformEvent,
JlDrawApp, JlDrawApp,
@ -15,25 +16,37 @@ import {
ISectionData, ISectionData,
Section, Section,
SectionConsts, SectionConsts,
SectionPort,
SectionTemplate, SectionTemplate,
SectionType,
} from './Section'; } from './Section';
import { import {
DisplayObject, DisplayObject,
FederatedMouseEvent, FederatedMouseEvent,
Graphics, Graphics,
IHitArea, IHitArea,
IPointData,
Point, Point,
} from 'pixi.js'; } from 'pixi.js';
import { import {
IEditPointOptions, IEditPointOptions,
ILineGraphic, ILineGraphic,
PolylineEditPlugin, PolylineEditPlugin,
addWayPoint,
clearWayPoint,
getWaypointRangeIndex,
} from 'src/jl-graphic/plugins/GraphicEditPlugin'; } from 'src/jl-graphic/plugins/GraphicEditPlugin';
import AbsorbablePoint, { import AbsorbablePoint, {
AbsorbableLine, AbsorbableLine,
AbsorbablePosition, AbsorbablePosition,
} from 'src/jl-graphic/graphic/AbsorbablePosition'; } from 'src/jl-graphic/graphic/AbsorbablePosition';
import { Turnout } from '../turnout/Turnout'; import { Turnout, TurnoutPort } from '../turnout/Turnout';
import { MenuItemOptions } from 'src/jl-graphic/ui/Menu';
import { ContextMenu } from 'src/jl-graphic/ui/ContextMenu';
import { Dialog } from 'quasar';
import { SectionData } from 'src/drawApp/graphics/SectionInteraction';
import { graphicData } from 'src/protos/stationLayoutGraphics';
import SectionSplitDialog from 'src/components/draw-app/dialogs/SectionSplitDialog.vue';
export class SectionDraw extends GraphicDrawAssistant< export class SectionDraw extends GraphicDrawAssistant<
SectionTemplate, SectionTemplate,
@ -46,7 +59,7 @@ export class SectionDraw extends GraphicDrawAssistant<
super(app, template, 'sym_o_timeline', '区段Section'); super(app, template, 'sym_o_timeline', '区段Section');
this.container.addChild(this.graphic); this.container.addChild(this.graphic);
SectionPointEditPlugin.init(app); SectionPointEditPlugin.init(app, this);
} }
onLeftDown(e: FederatedMouseEvent): void { onLeftDown(e: FederatedMouseEvent): void {
@ -88,20 +101,23 @@ export class SectionDraw extends GraphicDrawAssistant<
prepareData(data: ISectionData): boolean { prepareData(data: ISectionData): boolean {
data.points = this.points; data.points = this.points;
data.code = 'G000'; data.code = 'G000';
data.childTransforms?.push( console.log(data.points[1].x);
console.log(data.points[0].x);
data.childTransforms = [
new ChildTransform( new ChildTransform(
'label', 'label',
new GraphicTransform( new GraphicTransform(
{ {
x: data.points[1].x - data.points[0].x, x: data.points[0].x + (data.points[1].x - data.points[0].x) / 2,
y: data.points[1].y - data.points[0].y + 20, y:
data.points[0].y + (data.points[1].y - data.points[0].y) / 2 + 20,
}, },
{ x: 0, y: 0 }, { x: 0, y: 0 },
0, 0,
{ x: 0, y: 0 } { x: 0, y: 0 }
) )
) ),
); ];
return true; return true;
} }
@ -191,9 +207,71 @@ class SectionPolylineEditPlugin extends PolylineEditPlugin {
this.updateEditedPointsPosition(); this.updateEditedPointsPosition();
} }
reset(): void { setRelatedDrag() {
super.reset(); const len = this.editedPoints.length;
this.initLabels(); this.editedPoints.forEach((ep, i) => {
if (i === 0 || i === len - 1) {
let relations: GraphicRelation[];
if (i === 0) {
relations = this.graphic.relationManage
.getRelationsOfGraphic(this.graphic)
.filter(
(relation) =>
relation.getRelationParam(this.graphic).param === SectionPort.A
);
} else {
relations = this.graphic.relationManage
.getRelationsOfGraphic(this.graphic)
.filter(
(relation) =>
relation.getRelationParam(this.graphic).param === SectionPort.B
);
}
if (!relations.length) return;
const points: IPointData[] = [];
const otherGraphics = relations.map((relation) =>
relation.getOtherGraphic(this.graphic)
);
const otherPorts = relations.map(
(relation) => relation.getOtherRelationParam(this.graphic).param
);
otherGraphics.forEach((otherGraphic, i) => {
const otherPort = otherPorts[i];
if (otherGraphic instanceof Turnout) {
if (otherPort === TurnoutPort.A) {
points.push(
otherGraphic.datas.pointA[otherGraphic.datas.pointA.length - 1]
);
} else if (otherPort === TurnoutPort.B) {
points.push(
otherGraphic.datas.pointB[otherGraphic.datas.pointB.length - 1]
);
} else if (otherPort === TurnoutPort.C) {
points.push(
otherGraphic.datas.pointC[otherGraphic.datas.pointC.length - 1]
);
}
} else if (otherGraphic instanceof Section) {
if (otherPort === SectionPort.A) {
points.push(otherGraphic.datas.points[0]);
} else if (otherPort === SectionPort.B) {
points.push(
otherGraphic.datas.points[otherGraphic.datas.points.length - 1]
);
}
}
});
const transformingHandler = () => {
otherGraphics.forEach((otherGraphic, i) => {
const p = otherGraphic.canvasToLocalPoint(ep);
points[i].x = p.x;
points[i].y = p.y;
otherGraphic.repaint();
});
};
ep.on('transforming', transformingHandler);
}
});
} }
updateEditedPointsPosition() { updateEditedPointsPosition() {
@ -209,14 +287,39 @@ class SectionPolylineEditPlugin extends PolylineEditPlugin {
} }
} }
export const addWaypointConfig: MenuItemOptions = {
name: '添加路径点',
};
export const clearWaypointsConfig: MenuItemOptions = {
name: '清除所有路径点',
};
export const splitSectionConfig: MenuItemOptions = {
name: '拆分',
// disabled: true,
};
const SectionEditMenu: ContextMenu = ContextMenu.init({
name: '区段编辑菜单',
groups: [
{
items: [addWaypointConfig, clearWaypointsConfig],
},
{
items: [splitSectionConfig],
},
],
});
export class SectionPointEditPlugin extends GraphicInteractionPlugin<Section> { export class SectionPointEditPlugin extends GraphicInteractionPlugin<Section> {
static Name = 'SectionPointDrag'; static Name = 'SectionPointDrag';
drawAssistant: SectionDraw;
constructor(app: GraphicApp) { constructor(app: GraphicApp, da: SectionDraw) {
super(SectionPointEditPlugin.Name, app); super(SectionPointEditPlugin.Name, app);
this.drawAssistant = da;
app.registerMenu(SectionEditMenu);
} }
static init(app: GraphicApp) { static init(app: GraphicApp, da: SectionDraw) {
return new SectionPointEditPlugin(app); return new SectionPointEditPlugin(app, da);
} }
filter(...grahpics: JlGraphic[]): Section[] | undefined { filter(...grahpics: JlGraphic[]): Section[] | undefined {
return grahpics.filter((g) => g.type == Section.Type) as Section[]; return grahpics.filter((g) => g.type == Section.Type) as Section[];
@ -232,14 +335,99 @@ export class SectionPointEditPlugin extends GraphicInteractionPlugin<Section> {
g.labelGraphic.draggable = true; g.labelGraphic.draggable = true;
g.on('selected', this.onSelected, this); g.on('selected', this.onSelected, this);
g.on('unselected', this.onUnselected, this); g.on('unselected', this.onUnselected, this);
g.on('_rightclick', this.onContextMenu, this);
} }
unbind(g: Section): void { unbind(g: Section): void {
g.off('selected', this.onSelected, this); g.off('selected', this.onSelected, this);
g.off('unselected', this.onUnselected, this); g.off('unselected', this.onUnselected, this);
g.off('_rightclick', this.onContextMenu, this);
}
onContextMenu(e: FederatedMouseEvent) {
const target = e.target as DisplayObject;
const section = target.getGraphic() as Section;
this.app.updateSelected(section);
const p = section.screenToLocalPoint(e.global);
addWaypointConfig.handler = () => {
const linePoints = section.linePoints;
const { start, end } = getWaypointRangeIndex(
linePoints,
false,
p,
SectionConsts.lineWidth
);
addWayPoint(section, false, start, end, p);
};
clearWaypointsConfig.handler = () => {
clearWayPoint(section, false);
};
if (
section.datas.children &&
section.datas.sectionType === SectionType.Physical
) {
splitSectionConfig.disabled = false;
splitSectionConfig.handler = () => {
Dialog.create({
title: '拆分区段',
message: '请选择生成数量和方向',
component: SectionSplitDialog,
cancel: true,
persistent: true,
}).onOk((data: { num: number; dir: 'ltr' | 'rtl' }) => {
const { num, dir } = data;
const sectionData = section.datas;
const points = section.getSplitPoints(num);
const children: Section[] = [];
let codeAppend = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.slice(0, num);
if (
(dir === 'ltr' &&
sectionData.points[0].x >
sectionData.points[sectionData.points.length - 1].x) ||
(dir === 'rtl' &&
sectionData.points[0].x <
sectionData.points[sectionData.points.length - 1].x)
) {
codeAppend = codeAppend.split('').reverse().join('');
}
points.forEach((ps, i) => {
const data = new SectionData();
data.id = this.drawAssistant.nextId();
data.code = `${sectionData.code}-${codeAppend.charAt(i % 26)}`;
data.sectionType = SectionType.Logic;
data.points = ps.map(
(p) => new graphicData.Point({ x: p.x, y: p.y })
);
data.id = this.drawAssistant.nextId();
const g = this.drawAssistant.graphicTemplate.new();
g.loadData(data);
this.drawAssistant.storeGraphic(g);
children.push(g);
});
sectionData.children = children.map((g) => g.datas.id);
section.repaint();
section.buildRelation();
section.draggable = false;
children.forEach((c) => c.buildRelation());
this.app.updateSelected(...children);
});
};
} else {
splitSectionConfig.disabled = true;
}
SectionEditMenu.open(e.global);
} }
onSelected(g: DisplayObject): void { onSelected(g: DisplayObject): void {
const section = g as Section; const section = g as Section;
if (
section.datas.children.length > 0 &&
section.datas.sectionType === SectionType.Physical
) {
return;
}
let lep = section.getAssistantAppend<SectionPolylineEditPlugin>( let lep = section.getAssistantAppend<SectionPolylineEditPlugin>(
SectionPolylineEditPlugin.Name SectionPolylineEditPlugin.Name
); );
@ -248,6 +436,7 @@ export class SectionPointEditPlugin extends GraphicInteractionPlugin<Section> {
section.addAssistantAppend(lep); section.addAssistantAppend(lep);
} }
lep.showAll(); lep.showAll();
lep.setRelatedDrag();
} }
onUnselected(g: DisplayObject): void { onUnselected(g: DisplayObject): void {
const section = g as Section; const section = g as Section;

View File

@ -4,6 +4,7 @@ import {
GraphicApp, GraphicApp,
GraphicDrawAssistant, GraphicDrawAssistant,
GraphicInteractionPlugin, GraphicInteractionPlugin,
GraphicRelation,
GraphicTransformEvent, GraphicTransformEvent,
JlDrawApp, JlDrawApp,
JlGraphic, JlGraphic,
@ -31,7 +32,7 @@ import {
GraphicEditPlugin, GraphicEditPlugin,
getWaypointRangeIndex, getWaypointRangeIndex,
} from 'src/jl-graphic/plugins/GraphicEditPlugin'; } from 'src/jl-graphic/plugins/GraphicEditPlugin';
import { Section } from '../section/Section'; import { Section, SectionPort } from '../section/Section';
import AbsorbablePoint, { import AbsorbablePoint, {
AbsorbableLine, AbsorbableLine,
} from 'src/jl-graphic/graphic/AbsorbablePosition'; } from 'src/jl-graphic/graphic/AbsorbablePosition';
@ -338,8 +339,9 @@ export class TurnoutPointsInteractionPlugin extends GraphicInteractionPlugin<Tur
tep = new TurnoutEditPlugin(turnout, { onEditPointCreate }); tep = new TurnoutEditPlugin(turnout, { onEditPointCreate });
turnout.addAssistantAppend(tep); turnout.addAssistantAppend(tep);
} }
tep.reset(); // tep.reset();
tep.showAll(); tep.showAll();
tep.setRelatedDrag();
} }
onUnSelected(g: DisplayObject) { onUnSelected(g: DisplayObject) {
@ -380,6 +382,82 @@ export class TurnoutEditPlugin extends GraphicEditPlugin<Turnout> {
this.removeChildren(); this.removeChildren();
this.initEditPoints(); this.initEditPoints();
} }
hideAll(): void {
super.hideAll();
}
setRelatedDrag() {
this.editPoints.forEach((eps, i) => {
const ep = eps[eps.length - 1];
let relations: GraphicRelation[];
if (i === 0) {
relations = this.graphic.relationManage
.getRelationsOfGraphic(this.graphic)
.filter(
(relation) =>
relation.getRelationParam(this.graphic).param === TurnoutPort.A
);
} else if (i === 1) {
relations = this.graphic.relationManage
.getRelationsOfGraphic(this.graphic)
.filter(
(relation) =>
relation.getRelationParam(this.graphic).param === TurnoutPort.B
);
} else {
relations = this.graphic.relationManage
.getRelationsOfGraphic(this.graphic)
.filter(
(relation) =>
relation.getRelationParam(this.graphic).param === TurnoutPort.C
);
}
if (!relations.length) return;
const otherGraphics = relations.map((relation) =>
relation.getOtherGraphic(this.graphic)
);
console.log(otherGraphics);
const otherPorts = relations.map(
(relation) => relation.getOtherRelationParam(this.graphic).param
);
const point: IPointData[] = [];
otherGraphics.map((otherGraphic, i) => {
const otherPort = otherPorts[i];
if (otherGraphic instanceof Turnout) {
if (otherPort === TurnoutPort.A) {
point.push(
otherGraphic.datas.pointA[otherGraphic.datas.pointA.length - 1]
);
} else if (otherPort === TurnoutPort.B) {
point.push(
otherGraphic.datas.pointB[otherGraphic.datas.pointB.length - 1]
);
} else if (otherPort === TurnoutPort.C) {
point.push(
otherGraphic.datas.pointC[otherGraphic.datas.pointC.length - 1]
);
}
} else if (otherGraphic instanceof Section) {
if (otherPort === SectionPort.A) {
point.push(otherGraphic.datas.points[0]);
} else if (otherPort === SectionPort.B) {
point.push(
otherGraphic.datas.points[otherGraphic.datas.points.length - 1]
);
}
}
});
const transformingHandler = () => {
otherGraphics.forEach((otherGraphic, i) => {
const p = otherGraphic.canvasToLocalPoint(ep);
point[i].x = p.x;
point[i].y = p.y;
otherGraphic.repaint();
});
};
ep.on('transforming', transformingHandler);
});
}
initEditPoints() { initEditPoints() {
const cpA = this.graphic.localToCanvasPoints(...this.graphic.datas.pointA); const cpA = this.graphic.localToCanvasPoints(...this.graphic.datas.pointA);

View File

@ -32,13 +32,13 @@ export abstract class GraphicEditPlugin<
this.sortableChildren = true; this.sortableChildren = true;
this.graphic.on('transformstart', this.hideAll, this); this.graphic.on('transformstart', this.hideAll, this);
this.graphic.on('transformend', this.showAll, this); this.graphic.on('transformend', this.showAll, this);
this.graphic.on('repaint', this.showAll, this); this.graphic.on('repaint', this.updateEditedPointsPosition, this);
} }
destroy(options?: boolean | IDestroyOptions | undefined): void { destroy(options?: boolean | IDestroyOptions | undefined): void {
this.graphic.off('transformstart', this.hideAll, this); this.graphic.off('transformstart', this.hideAll, this);
this.graphic.off('transformend', this.showAll, this); this.graphic.off('transformend', this.showAll, this);
this.graphic.off('repaint', this.showAll, this); this.graphic.off('repaint', this.updateEditedPointsPosition, this);
super.destroy(options); super.destroy(options);
} }

View File

@ -188,7 +188,7 @@ export namespace state {
return Section.deserialize(bytes); return Section.deserialize(bytes);
} }
} }
export class Switch extends pb_1.Message { export class Turnout extends pb_1.Message {
#one_of_decls: number[][] = []; #one_of_decls: number[][] = [];
constructor(data?: any[] | { constructor(data?: any[] | {
id?: string; id?: string;
@ -242,8 +242,8 @@ export namespace state {
code?: string; code?: string;
kilometerSystem?: ReturnType<typeof dependency_1.graphicData.KilometerSystem.prototype.toObject>[]; kilometerSystem?: ReturnType<typeof dependency_1.graphicData.KilometerSystem.prototype.toObject>[];
convertKilometer?: number[]; convertKilometer?: number[];
}): Switch { }): Turnout {
const message = new Switch({}); const message = new Turnout({});
if (data.id != null) { if (data.id != null) {
message.id = data.id; message.id = data.id;
} }
@ -294,8 +294,8 @@ export namespace state {
if (!w) if (!w)
return writer.getResultBuffer(); return writer.getResultBuffer();
} }
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): Switch { static deserialize(bytes: Uint8Array | pb_1.BinaryReader): Turnout {
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new Switch(); const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new Turnout();
while (reader.nextField()) { while (reader.nextField()) {
if (reader.isEndGroup()) if (reader.isEndGroup())
break; break;
@ -320,8 +320,8 @@ export namespace state {
serializeBinary(): Uint8Array { serializeBinary(): Uint8Array {
return this.serialize(); return this.serialize();
} }
static deserializeBinary(bytes: Uint8Array): Switch { static deserializeBinary(bytes: Uint8Array): Turnout {
return Switch.deserialize(bytes); return Turnout.deserialize(bytes);
} }
} }
} }

@ -1 +1 @@
Subproject commit 549aa2ec10bffe292a1a68e278ae824a8502db0b Subproject commit 8fd000d45907f94410786c3bd6c1ab37edbe91e8