Neu gezeichnete Elemente treten der aktiven Gruppe bei; Fix: m/s/d bei Mehrfachauswahl
Zeichnet man ein neues Element, während man eine Gruppe betreten hat (Isolation), wird es automatisch Mitglied dieser (innersten) Gruppe — man zeichnet ja sichtlich "in" ihr. Vergleicht vor/nach jedem Commit über alle Elementarten auf neu hinzugekommene IDs, additiv zur eigentlichen Mutation. Separater Fix: Bewegen/Kopieren/Spiegeln/Drehen/Feld (m/s/d-Kürzel und die gleichnamigen Befehle) funktionierten nicht, sobald mehr als ein 2D-Element gewählt war — TransformSelection kannte nur ein einzelnes `drawingId`, das bei Mehrfachauswahl `null` ist. Neues `drawingIds`-Feld (Union mit `drawingId`, dedupliziert) durchgereicht von allen Aufrufstellen (Move/ Copy/Mirror/Array-Befehle, Tastatur-Kürzel-Pfad).
This commit is contained in:
+14
-2
@@ -115,7 +115,7 @@ import { useGripEditing } from "./state/useGripEditing";
|
|||||||
import { usePanelHostBase } from "./state/usePanelHostBase";
|
import { usePanelHostBase } from "./state/usePanelHostBase";
|
||||||
import { useDockDragState } from "./state/useDockDragState";
|
import { useDockDragState } from "./state/useDockDragState";
|
||||||
import { useMouseSelectionHandlers } from "./state/useMouseSelectionHandlers";
|
import { useMouseSelectionHandlers } from "./state/useMouseSelectionHandlers";
|
||||||
import { containingGroupId, EMPTY_MEMBERSHIP } from "./state/groupActions";
|
import { addNewElementsToActiveGroup, containingGroupId, EMPTY_MEMBERSHIP } from "./state/groupActions";
|
||||||
import type { GroupMembership } from "./state/groupActions";
|
import type { GroupMembership } from "./state/groupActions";
|
||||||
import { useProjectFileActions } from "./state/useProjectFileActions";
|
import { useProjectFileActions } from "./state/useProjectFileActions";
|
||||||
import { useProjectActions } from "./state/useProjectActions";
|
import { useProjectActions } from "./state/useProjectActions";
|
||||||
@@ -621,6 +621,7 @@ export default function App() {
|
|||||||
const transformSel: TransformSelection = {
|
const transformSel: TransformSelection = {
|
||||||
wallIds: selectedWallIds,
|
wallIds: selectedWallIds,
|
||||||
drawingId: selectedDrawingId,
|
drawingId: selectedDrawingId,
|
||||||
|
drawingIds: selectedDrawingIds,
|
||||||
extrudedSolidId: selectedExtrudedSolidId,
|
extrudedSolidId: selectedExtrudedSolidId,
|
||||||
columnId: selectedColumnId,
|
columnId: selectedColumnId,
|
||||||
};
|
};
|
||||||
@@ -745,6 +746,7 @@ export default function App() {
|
|||||||
selectedDrawingIds,
|
selectedDrawingIds,
|
||||||
selectedExtrudedSolidId,
|
selectedExtrudedSolidId,
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
|
groupPath,
|
||||||
setProject,
|
setProject,
|
||||||
setDraft,
|
setDraft,
|
||||||
});
|
});
|
||||||
@@ -761,6 +763,7 @@ export default function App() {
|
|||||||
selectedDrawingIds,
|
selectedDrawingIds,
|
||||||
selectedExtrudedSolidId,
|
selectedExtrudedSolidId,
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
|
groupPath,
|
||||||
setProject,
|
setProject,
|
||||||
setDraft,
|
setDraft,
|
||||||
};
|
};
|
||||||
@@ -808,7 +811,15 @@ export default function App() {
|
|||||||
},
|
},
|
||||||
pxPerMeter: () => planViewRef.current?.pxPerMeter() ?? 90,
|
pxPerMeter: () => planViewRef.current?.pxPerMeter() ?? 90,
|
||||||
setDraft: (d) => cmdLatest.current.setDraft(d),
|
setDraft: (d) => cmdLatest.current.setDraft(d),
|
||||||
commit: (mutate) => cmdLatest.current.setProject(mutate),
|
// Zeichnet man WÄHREND man eine Gruppe betreten hat (Isolation), wird
|
||||||
|
// das neue Element automatisch Mitglied dieser Gruppe (Nutzer-Wunsch:
|
||||||
|
// „wenn man etwas neu zeichnet ... zeichnet man in der Gruppe") — s.
|
||||||
|
// state/groupActions.ts::addNewElementsToActiveGroup (Diff vor/nach
|
||||||
|
// Commit über alle Elementarten, additiv zur eigentlichen Mutation).
|
||||||
|
commit: (mutate) =>
|
||||||
|
cmdLatest.current.setProject((p) =>
|
||||||
|
addNewElementsToActiveGroup(p, mutate(p), cmdLatest.current.groupPath),
|
||||||
|
),
|
||||||
isFloor: () => cmdLatest.current.activeLevel.kind === "floor",
|
isFloor: () => cmdLatest.current.activeLevel.kind === "floor",
|
||||||
// Frisch über `text`/`textbox` erzeugtes Element sofort inline editieren
|
// Frisch über `text`/`textbox` erzeugtes Element sofort inline editieren
|
||||||
// (kein Lookup nötig — der Commit hat es bereits angehängt, s. engine.ts
|
// (kein Lookup nötig — der Commit hat es bereits angehängt, s. engine.ts
|
||||||
@@ -1270,6 +1281,7 @@ export default function App() {
|
|||||||
setActiveTransform,
|
setActiveTransform,
|
||||||
selectedWallIds,
|
selectedWallIds,
|
||||||
selectedDrawingId,
|
selectedDrawingId,
|
||||||
|
selectedDrawingIds,
|
||||||
selectedExtrudedSolidId,
|
selectedExtrudedSolidId,
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
engine,
|
engine,
|
||||||
|
|||||||
@@ -49,12 +49,19 @@ function targetFromFields(base: Vec2, locks: Record<string, number>, cursor: Vec
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasSelection(sel: CommandSelection): boolean {
|
function hasSelection(sel: CommandSelection): boolean {
|
||||||
return sel.wallIds.length > 0 || sel.drawingId !== null || !!sel.extrudedSolidId || !!sel.columnId;
|
return (
|
||||||
|
sel.wallIds.length > 0 ||
|
||||||
|
sel.drawingId !== null ||
|
||||||
|
(sel.drawingIds?.length ?? 0) > 0 ||
|
||||||
|
!!sel.extrudedSolidId ||
|
||||||
|
!!sel.columnId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
function toTransformSel(sel: CommandSelection): TransformSelection {
|
function toTransformSel(sel: CommandSelection): TransformSelection {
|
||||||
return {
|
return {
|
||||||
wallIds: sel.wallIds,
|
wallIds: sel.wallIds,
|
||||||
drawingId: sel.drawingId,
|
drawingId: sel.drawingId,
|
||||||
|
drawingIds: sel.drawingIds,
|
||||||
extrudedSolidId: sel.extrudedSolidId,
|
extrudedSolidId: sel.extrudedSolidId,
|
||||||
columnId: sel.columnId,
|
columnId: sel.columnId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ function hasSelection(sel: CommandSelection): boolean {
|
|||||||
return (
|
return (
|
||||||
sel.wallIds.length > 0 ||
|
sel.wallIds.length > 0 ||
|
||||||
sel.drawingId !== null ||
|
sel.drawingId !== null ||
|
||||||
|
(sel.drawingIds?.length ?? 0) > 0 ||
|
||||||
!!sel.extrudedSolidId ||
|
!!sel.extrudedSolidId ||
|
||||||
!!sel.columnId
|
!!sel.columnId
|
||||||
);
|
);
|
||||||
@@ -53,6 +54,7 @@ function toTransformSel(sel: CommandSelection): TransformSelection {
|
|||||||
return {
|
return {
|
||||||
wallIds: sel.wallIds,
|
wallIds: sel.wallIds,
|
||||||
drawingId: sel.drawingId,
|
drawingId: sel.drawingId,
|
||||||
|
drawingIds: sel.drawingIds,
|
||||||
extrudedSolidId: sel.extrudedSolidId,
|
extrudedSolidId: sel.extrudedSolidId,
|
||||||
columnId: sel.columnId,
|
columnId: sel.columnId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ function hasSelection(sel: CommandSelection): boolean {
|
|||||||
return (
|
return (
|
||||||
sel.wallIds.length > 0 ||
|
sel.wallIds.length > 0 ||
|
||||||
sel.drawingId !== null ||
|
sel.drawingId !== null ||
|
||||||
|
(sel.drawingIds?.length ?? 0) > 0 ||
|
||||||
!!sel.extrudedSolidId ||
|
!!sel.extrudedSolidId ||
|
||||||
!!sel.columnId
|
!!sel.columnId
|
||||||
);
|
);
|
||||||
@@ -45,6 +46,7 @@ function toTransformSel(sel: CommandSelection): TransformSelection {
|
|||||||
return {
|
return {
|
||||||
wallIds: sel.wallIds,
|
wallIds: sel.wallIds,
|
||||||
drawingId: sel.drawingId,
|
drawingId: sel.drawingId,
|
||||||
|
drawingIds: sel.drawingIds,
|
||||||
extrudedSolidId: sel.extrudedSolidId,
|
extrudedSolidId: sel.extrudedSolidId,
|
||||||
columnId: sel.columnId,
|
columnId: sel.columnId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function hasSelection(sel: CommandSelection): boolean {
|
|||||||
return (
|
return (
|
||||||
sel.wallIds.length > 0 ||
|
sel.wallIds.length > 0 ||
|
||||||
sel.drawingId !== null ||
|
sel.drawingId !== null ||
|
||||||
|
(sel.drawingIds?.length ?? 0) > 0 ||
|
||||||
!!sel.extrudedSolidId ||
|
!!sel.extrudedSolidId ||
|
||||||
!!sel.columnId
|
!!sel.columnId
|
||||||
);
|
);
|
||||||
@@ -62,6 +63,7 @@ function toTransformSel(sel: CommandSelection): TransformSelection {
|
|||||||
return {
|
return {
|
||||||
wallIds: sel.wallIds,
|
wallIds: sel.wallIds,
|
||||||
drawingId: sel.drawingId,
|
drawingId: sel.drawingId,
|
||||||
|
drawingIds: sel.drawingIds,
|
||||||
extrudedSolidId: sel.extrudedSolidId,
|
extrudedSolidId: sel.extrudedSolidId,
|
||||||
columnId: sel.columnId,
|
columnId: sel.columnId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
addNewElementsToActiveGroup,
|
||||||
applyGroupDimming,
|
applyGroupDimming,
|
||||||
containingGroupId,
|
containingGroupId,
|
||||||
expandToGroups,
|
expandToGroups,
|
||||||
@@ -185,6 +186,51 @@ describe("applyGroupDimming", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("addNewElementsToActiveGroup", () => {
|
||||||
|
const g1: Group = { id: "g1", wallIds: ["w1"], drawingIds: [], ceilingIds: [], roomIds: [], stairIds: [], columnIds: [], roofIds: [], openingIds: [], groupIds: [] };
|
||||||
|
const line = (id: string) =>
|
||||||
|
({ id, type: "drawing2d", levelId: "eg", categoryCode: "20", geom: { shape: "line", a: { x: 0, y: 0 }, b: { x: 1, y: 0 } } }) as Project["drawings2d"][number];
|
||||||
|
|
||||||
|
it("neu gezeichnetes Element wird Mitglied der aktiven (innersten) Gruppe", () => {
|
||||||
|
const before = project([g1]);
|
||||||
|
const after: Project = { ...before, drawings2d: [line("d1")] };
|
||||||
|
const result = addNewElementsToActiveGroup(before, after, ["g1"]);
|
||||||
|
const g = result.groups!.find((x) => x.id === "g1")!;
|
||||||
|
expect(g.drawingIds).toEqual(["d1"]);
|
||||||
|
expect(g.wallIds).toEqual(["w1"]); // bestehende Mitgliedschaft bleibt
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ohne aktive Gruppe (groupPath leer) bleibt das Projekt unverändert", () => {
|
||||||
|
const before = project([g1]);
|
||||||
|
const after: Project = { ...before, drawings2d: [line("d1")] };
|
||||||
|
const result = addNewElementsToActiveGroup(before, after, []);
|
||||||
|
expect(result).toBe(after); // unverändert (Referenzgleichheit)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keine Änderung (before === after) ist ein No-op", () => {
|
||||||
|
const p = project([g1]);
|
||||||
|
expect(addNewElementsToActiveGroup(p, p, ["g1"])).toBe(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mehrere neue Elemente auf einmal (z. B. Feld/Array) werden alle Mitglied", () => {
|
||||||
|
const before = project([g1]);
|
||||||
|
const after: Project = { ...before, drawings2d: [line("d1"), line("d2"), line("d3")] };
|
||||||
|
const result = addNewElementsToActiveGroup(before, after, ["g1"]);
|
||||||
|
const g = result.groups!.find((x) => x.id === "g1")!;
|
||||||
|
expect(g.drawingIds.sort()).toEqual(["d1", "d2", "d3"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verschachtelt: neues Element landet in der INNERSTEN (aktiven) Gruppe, nicht der äusseren", () => {
|
||||||
|
const inner: Group = { id: "g1", wallIds: [], drawingIds: [], ceilingIds: [], roomIds: [], stairIds: [], columnIds: [], roofIds: [], openingIds: [], groupIds: [] };
|
||||||
|
const outer: Group = { id: "g2", wallIds: [], drawingIds: [], ceilingIds: [], roomIds: [], stairIds: [], columnIds: [], roofIds: [], openingIds: [], groupIds: ["g1"] };
|
||||||
|
const before = project([inner, outer]);
|
||||||
|
const after: Project = { ...before, drawings2d: [line("d1")] };
|
||||||
|
const result = addNewElementsToActiveGroup(before, after, ["g2", "g1"]);
|
||||||
|
expect(result.groups!.find((x) => x.id === "g1")!.drawingIds).toEqual(["d1"]);
|
||||||
|
expect(result.groups!.find((x) => x.id === "g2")!.drawingIds).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("ungroup", () => {
|
describe("ungroup", () => {
|
||||||
it("löst genau EIN Level auf; Untergruppen bleiben als eigene Gruppen bestehen", () => {
|
it("löst genau EIN Level auf; Untergruppen bleiben als eigene Gruppen bestehen", () => {
|
||||||
const inner: Group = { id: "g1", wallIds: ["w1"], drawingIds: [], ceilingIds: [], roomIds: [], stairIds: [], columnIds: [], roofIds: [], openingIds: [], groupIds: [] };
|
const inner: Group = { id: "g1", wallIds: ["w1"], drawingIds: [], ceilingIds: [], roomIds: [], stairIds: [], columnIds: [], roofIds: [], openingIds: [], groupIds: [] };
|
||||||
|
|||||||
@@ -227,6 +227,47 @@ export function applyGroupDimming(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** IDs, die in `after` neu hinzugekommen sind (in `before` noch nicht vorhanden). */
|
||||||
|
function newIds(before: { id: string }[], after: { id: string }[]): string[] {
|
||||||
|
const beforeIds = new Set(before.map((x) => x.id));
|
||||||
|
return after.filter((x) => !beforeIds.has(x.id)).map((x) => x.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeichnet man WÄHREND man eine Gruppe betreten hat (Isolation) ein neues
|
||||||
|
* Element, wird es automatisch Mitglied dieser (innersten) Gruppe — man
|
||||||
|
* zeichnet ja SICHTLICH „in" ihr (Nutzer-Wunsch, Vectorworks-Verhalten).
|
||||||
|
* Vergleicht `before`/`after` je Elementart auf neu hinzugekommene IDs (Diff
|
||||||
|
* über die commit-Mutation) und hängt sie an die aktive Gruppe an. `before`
|
||||||
|
* === `after` (keine Änderung) oder `groupPath` leer → `after` unverändert.
|
||||||
|
*/
|
||||||
|
export function addNewElementsToActiveGroup(
|
||||||
|
before: Project,
|
||||||
|
after: Project,
|
||||||
|
groupPath: string[],
|
||||||
|
): Project {
|
||||||
|
if (groupPath.length === 0 || before === after) return after;
|
||||||
|
const activeId = groupPath[groupPath.length - 1];
|
||||||
|
const g = (after.groups ?? []).find((x) => x.id === activeId);
|
||||||
|
if (!g) return after;
|
||||||
|
const added: GroupMembership = {
|
||||||
|
wallIds: newIds(before.walls, after.walls),
|
||||||
|
drawingIds: newIds(before.drawings2d, after.drawings2d),
|
||||||
|
ceilingIds: newIds(before.ceilings ?? [], after.ceilings ?? []),
|
||||||
|
roomIds: newIds(before.rooms ?? [], after.rooms ?? []),
|
||||||
|
stairIds: newIds(before.stairs ?? [], after.stairs ?? []),
|
||||||
|
columnIds: newIds(before.columns ?? [], after.columns ?? []),
|
||||||
|
roofIds: newIds(before.roofs ?? [], after.roofs ?? []),
|
||||||
|
openingIds: newIds(before.openings ?? [], after.openings ?? []),
|
||||||
|
};
|
||||||
|
if (KEYS.every((k) => added[k].length === 0)) return after;
|
||||||
|
const merged: Group = { ...g, ...mergeMembership(g, added) };
|
||||||
|
return {
|
||||||
|
...after,
|
||||||
|
groups: (after.groups ?? []).map((x) => (x.id === activeId ? merged : x)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erzeugt eine neue Gruppe aus einer Mengen-Auswahl (+ optional bereits
|
* Erzeugt eine neue Gruppe aus einer Mengen-Auswahl (+ optional bereits
|
||||||
* gewählte Gruppen, die dabei als Untergruppen verschachtelt werden — „zwei
|
* gewählte Gruppen, die dabei als Untergruppen verschachtelt werden — „zwei
|
||||||
|
|||||||
@@ -502,6 +502,7 @@ export function useTransformShortcuts(
|
|||||||
) => void,
|
) => void,
|
||||||
selectedWallIds: string[],
|
selectedWallIds: string[],
|
||||||
selectedDrawingId: string | null,
|
selectedDrawingId: string | null,
|
||||||
|
selectedDrawingIds: string[],
|
||||||
selectedExtrudedSolidId: string | null,
|
selectedExtrudedSolidId: string | null,
|
||||||
selectedColumnId: string | null,
|
selectedColumnId: string | null,
|
||||||
engine: CommandEngine,
|
engine: CommandEngine,
|
||||||
@@ -543,6 +544,7 @@ export function useTransformShortcuts(
|
|||||||
const hasSel =
|
const hasSel =
|
||||||
selectedWallIds.length > 0 ||
|
selectedWallIds.length > 0 ||
|
||||||
!!selectedDrawingId ||
|
!!selectedDrawingId ||
|
||||||
|
selectedDrawingIds.length > 0 ||
|
||||||
!!selectedExtrudedSolidId ||
|
!!selectedExtrudedSolidId ||
|
||||||
!!selectedColumnId;
|
!!selectedColumnId;
|
||||||
if (!hasSel) return;
|
if (!hasSel) return;
|
||||||
@@ -567,6 +569,7 @@ export function useTransformShortcuts(
|
|||||||
activeTransform,
|
activeTransform,
|
||||||
selectedWallIds,
|
selectedWallIds,
|
||||||
selectedDrawingId,
|
selectedDrawingId,
|
||||||
|
selectedDrawingIds,
|
||||||
selectedExtrudedSolidId,
|
selectedExtrudedSolidId,
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
engine,
|
engine,
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* `commitTransform`/`transformPreview` mit MEHREREN gewählten 2D-Elementen
|
||||||
|
* (`TransformSelection.drawingIds`) — Nutzer-Report: "wenn ich zwei Linien
|
||||||
|
* anwähle, dass ich dann m usw nicht verwenden kann". Vorher trug
|
||||||
|
* `TransformSelection` nur ein einzelnes `drawingId`, das bei einer
|
||||||
|
* Mehrfachauswahl (2+ Elemente) `null` war → Bewegen/Kopieren/Spiegeln/
|
||||||
|
* Drehen/Feld griffen gar nicht.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { commitTransform } from "./transform";
|
||||||
|
import type { TransformSelection } from "./transform";
|
||||||
|
import type { Drawing2D, Project } from "../model/types";
|
||||||
|
|
||||||
|
function project(drawings: Drawing2D[]): Project {
|
||||||
|
return {
|
||||||
|
id: "t",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [],
|
||||||
|
hatches: [],
|
||||||
|
components: [],
|
||||||
|
wallTypes: [],
|
||||||
|
drawingLevels: [],
|
||||||
|
layers: [],
|
||||||
|
walls: [],
|
||||||
|
doors: [],
|
||||||
|
drawings2d: drawings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function line(id: string, x: number): Drawing2D {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "line", a: { x, y: 0 }, b: { x: x + 1, y: 0 } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("commitTransform — Mehrfachauswahl (drawingIds)", () => {
|
||||||
|
it("move: verschiebt ALLE gewählten 2D-Elemente, nicht nur eines", () => {
|
||||||
|
const p = project([line("d1", 0), line("d2", 5), line("d3", 10)]);
|
||||||
|
const sel: TransformSelection = { wallIds: [], drawingId: null, drawingIds: ["d1", "d2"] };
|
||||||
|
const next = commitTransform(p, sel, "move", [{ x: 0, y: 0 }, { x: 2, y: 3 }], "move", 1);
|
||||||
|
const d1 = next.drawings2d.find((d) => d.id === "d1")!.geom;
|
||||||
|
const d2 = next.drawings2d.find((d) => d.id === "d2")!.geom;
|
||||||
|
const d3 = next.drawings2d.find((d) => d.id === "d3")!.geom;
|
||||||
|
if (d1.shape === "line") expect(d1.a).toEqual({ x: 2, y: 3 });
|
||||||
|
if (d2.shape === "line") expect(d2.a).toEqual({ x: 7, y: 3 }); // 5+2
|
||||||
|
if (d3.shape === "line") expect(d3.a).toEqual({ x: 10, y: 0 }); // nicht gewählt, unverändert
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copy: dupliziert ALLE gewählten 2D-Elemente (Original bleibt)", () => {
|
||||||
|
const p = project([line("d1", 0), line("d2", 5)]);
|
||||||
|
const sel: TransformSelection = { wallIds: [], drawingId: null, drawingIds: ["d1", "d2"] };
|
||||||
|
const next = commitTransform(p, sel, "move", [{ x: 0, y: 0 }, { x: 1, y: 0 }], "copy", 1);
|
||||||
|
expect(next.drawings2d.length).toBe(4); // 2 Original + 2 Kopien
|
||||||
|
});
|
||||||
|
|
||||||
|
it("array: dupliziert ALLE gewählten 2D-Elemente N-mal", () => {
|
||||||
|
const p = project([line("d1", 0), line("d2", 5)]);
|
||||||
|
const sel: TransformSelection = { wallIds: [], drawingId: null, drawingIds: ["d1", "d2"] };
|
||||||
|
const next = commitTransform(p, sel, "move", [{ x: 0, y: 0 }, { x: 1, y: 0 }], "array", 3);
|
||||||
|
expect(next.drawings2d.length).toBe(8); // 2 Original + 2×3 Kopien
|
||||||
|
});
|
||||||
|
|
||||||
|
it("weiterhin abwärtskompatibel: einzelnes `drawingId` (ohne drawingIds) funktioniert unverändert", () => {
|
||||||
|
const p = project([line("d1", 0)]);
|
||||||
|
const sel: TransformSelection = { wallIds: [], drawingId: "d1" };
|
||||||
|
const next = commitTransform(p, sel, "move", [{ x: 0, y: 0 }, { x: 3, y: 0 }], "move", 1);
|
||||||
|
const d1 = next.drawings2d[0].geom;
|
||||||
|
if (d1.shape === "line") expect(d1.a).toEqual({ x: 3, y: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drawingId + drawingIds zusammen werden dedupliziert (kein Doppel-Verschieben)", () => {
|
||||||
|
const p = project([line("d1", 0), line("d2", 5)]);
|
||||||
|
const sel: TransformSelection = { wallIds: [], drawingId: "d1", drawingIds: ["d1", "d2"] };
|
||||||
|
const next = commitTransform(p, sel, "move", [{ x: 0, y: 0 }, { x: 1, y: 0 }], "copy", 1);
|
||||||
|
expect(next.drawings2d.length).toBe(4); // je EINE Kopie pro Element, nicht zwei für d1
|
||||||
|
});
|
||||||
|
});
|
||||||
+19
-6
@@ -30,13 +30,27 @@ export type CopyMode = "move" | "copy" | "array" | "distribute"; // U / I / O /
|
|||||||
/** Welche Auswahl transformiert wird. */
|
/** Welche Auswahl transformiert wird. */
|
||||||
export interface TransformSelection {
|
export interface TransformSelection {
|
||||||
wallIds: string[];
|
wallIds: string[];
|
||||||
|
/**
|
||||||
|
* Einzeln gewähltes 2D-Element (Legacy-Feld, s. `CommandSelection.drawingId`).
|
||||||
|
* `drawingIds` (Mehrfachauswahl) wird bevorzugt ausgewertet; beide zusammen
|
||||||
|
* ergeben die MENGE der zu transformierenden 2D-Elemente (Union, dedupliziert).
|
||||||
|
*/
|
||||||
drawingId: string | null;
|
drawingId: string | null;
|
||||||
|
/** ALLE gewählten 2D-Elemente (Mehrfachauswahl) — s. `CommandSelection.drawingIds`. */
|
||||||
|
drawingIds?: string[];
|
||||||
/** Gewählter extrudierter Körper (truck-Integration); nur move.ts setzt ihn. */
|
/** Gewählter extrudierter Körper (truck-Integration); nur move.ts setzt ihn. */
|
||||||
extrudedSolidId?: string | null;
|
extrudedSolidId?: string | null;
|
||||||
/** Gewählte Stütze (Tragwerk); von move/copy/mirror gesetzt. */
|
/** Gewählte Stütze (Tragwerk); von move/copy/mirror gesetzt. */
|
||||||
columnId?: string | null;
|
columnId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Vereinigt `drawingId`/`drawingIds` zu einer eindeutigen ID-Menge (dedupliziert). */
|
||||||
|
function drawingIdSet(sel: TransformSelection): Set<string> {
|
||||||
|
const ids = new Set(sel.drawingIds ?? []);
|
||||||
|
if (sel.drawingId) ids.add(sel.drawingId);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
/** Wie viele Klickpunkte die Operation braucht (Drehen = 3, sonst 2). */
|
/** Wie viele Klickpunkte die Operation braucht (Drehen = 3, sonst 2). */
|
||||||
export const requiredPoints = (op: TransformOp): number => (op === "rotate" ? 3 : 2);
|
export const requiredPoints = (op: TransformOp): number => (op === "rotate" ? 3 : 2);
|
||||||
|
|
||||||
@@ -116,10 +130,8 @@ function gatherItems(project: Project, sel: TransformSelection): AnyItem[] {
|
|||||||
const items: AnyItem[] = [];
|
const items: AnyItem[] = [];
|
||||||
const ids = new Set(sel.wallIds);
|
const ids = new Set(sel.wallIds);
|
||||||
for (const w of project.walls) if (ids.has(w.id)) items.push({ kind: "wall", wall: w });
|
for (const w of project.walls) if (ids.has(w.id)) items.push({ kind: "wall", wall: w });
|
||||||
if (sel.drawingId) {
|
const drawIds = drawingIdSet(sel);
|
||||||
const d = project.drawings2d.find((x) => x.id === sel.drawingId);
|
for (const d of project.drawings2d) if (drawIds.has(d.id)) items.push({ kind: "drawing", drawing: d });
|
||||||
if (d) items.push({ kind: "drawing", drawing: d });
|
|
||||||
}
|
|
||||||
if (sel.extrudedSolidId) {
|
if (sel.extrudedSolidId) {
|
||||||
const s = (project.extrudedSolids ?? []).find((x) => x.id === sel.extrudedSolidId);
|
const s = (project.extrudedSolids ?? []).find((x) => x.id === sel.extrudedSolidId);
|
||||||
if (s) items.push({ kind: "extrudedSolid", solid: s });
|
if (s) items.push({ kind: "extrudedSolid", solid: s });
|
||||||
@@ -292,9 +304,10 @@ export function commitTransform(
|
|||||||
if (movesOriginal(mode)) {
|
if (movesOriginal(mode)) {
|
||||||
const fn = transformFn(op, pts, 1);
|
const fn = transformFn(op, pts, 1);
|
||||||
walls = walls.map((w) => (wallIds.has(w.id) ? { ...w, start: fn(w.start), end: fn(w.end) } : w));
|
walls = walls.map((w) => (wallIds.has(w.id) ? { ...w, start: fn(w.start), end: fn(w.end) } : w));
|
||||||
if (sel.drawingId) {
|
const moveDrawIds = drawingIdSet(sel);
|
||||||
|
if (moveDrawIds.size > 0) {
|
||||||
drawings2d = drawings2d.map((d) =>
|
drawings2d = drawings2d.map((d) =>
|
||||||
d.id === sel.drawingId ? { ...d, geom: mvGeom(d.geom, fn) } : d,
|
moveDrawIds.has(d.id) ? { ...d, geom: mvGeom(d.geom, fn) } : d,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (sel.extrudedSolidId) {
|
if (sel.extrudedSolidId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user