diff --git a/src/app/components/Grid/compound-link/compound-link.component.ts b/src/app/components/Grid/compound-link/compound-link.component.ts
index 0164631b..582ce87b 100644
--- a/src/app/components/Grid/compound-link/compound-link.component.ts
+++ b/src/app/components/Grid/compound-link/compound-link.component.ts
@@ -12,6 +12,7 @@ import { UnitConversionService } from 'src/app/services/unit-conversion.service'
import { NotificationService } from 'src/app/services/notification.service';
import { AnimationService } from 'src/app/services/animation.service';
+import {UndoRedoService} from "../../../services/undo-redo.service";
@Component({
selector: '[app-compound-link]',
@@ -27,13 +28,14 @@ export class CompoundLinkComponent extends AbstractInteractiveComponent {
private svgPathService: SVGPathService,
private unitConversionService: UnitConversionService,
private notificationService: NotificationService,
- public override animationService: AnimationService
+ public override animationService: AnimationService,
+ private undoRedoService: UndoRedoService
) {
super(interactionService,animationService);
}
override registerInteractor(): Interactor {
- return new CompoundLinkInteractor(this.compoundLink, this.stateService, this.notificationService);
+ return new CompoundLinkInteractor(this.compoundLink, this.stateService, this.notificationService, this.undoRedoService);
}
getColor():string{
@@ -49,7 +51,8 @@ export class CompoundLinkComponent extends AbstractInteractiveComponent {
let allCoordsAsArray: Coord[] = Array.from(allUniqueJointCoords,(coord,index) =>{
return this.unitConversionService.modelCoordToSVGCoord(coord);
});
- return this.svgPathService.getSingleLinkDrawnPath(allCoordsAsArray, radius);
+ return this.svgPathService.calculateCompoundPath(this.compoundLink.links, allCoordsAsArray, radius);
+ //return this.svgPathService.getSingleLinkDrawnPath(allCoordsAsArray, radius);
}
getStrokeColor(): string{
if (this.getInteractor().isSelected) {
diff --git a/src/app/components/Grid/graph/graph.component.html b/src/app/components/Grid/graph/graph.component.html
index 6cc560c4..422ff9c8 100644
--- a/src/app/components/Grid/graph/graph.component.html
+++ b/src/app/components/Grid/graph/graph.component.html
@@ -9,7 +9,7 @@
-
+
diff --git a/src/app/components/Grid/graph/graph.component.ts b/src/app/components/Grid/graph/graph.component.ts
index cf7eee07..7e4badfd 100644
--- a/src/app/components/Grid/graph/graph.component.ts
+++ b/src/app/components/Grid/graph/graph.component.ts
@@ -113,6 +113,15 @@ export class GraphComponent {
return Array.from(this.stateService.getMechanism().getTrajectories());
}
+ public getOneJoint(id: number): Joint | null {
+ for (const joint of this.stateService.getMechanism().getJoints()) {
+ if (joint.id === id) {
+ return joint;
+ }
+ }
+ return null;
+ }
+
public getLinks(): Link[] {
return Array.from(this.stateService.getMechanism().getIndependentLinks());
}
diff --git a/src/app/components/Grid/joint/joint.component.css b/src/app/components/Grid/joint/joint.component.css
index 7492bee3..f4906834 100644
--- a/src/app/components/Grid/joint/joint.component.css
+++ b/src/app/components/Grid/joint/joint.component.css
@@ -9,5 +9,5 @@
.weld-icon{
padding: 3px;
- filter: invert(100%) sepia(100%) saturate(1%) hue-rotate(343deg) brightness(102%) contrast(101%);
+ filter: sepia(100%) saturate(1%) hue-rotate(343deg) brightness(102%) contrast(101%);
}
diff --git a/src/app/components/Grid/joint/joint.component.html b/src/app/components/Grid/joint/joint.component.html
index 00baab50..e40eeec2 100644
--- a/src/app/components/Grid/joint/joint.component.html
+++ b/src/app/components/Grid/joint/joint.component.html
@@ -52,27 +52,7 @@
/>
}
-
- @if(isWelded()){
-
-
-
-
-
- }
- @if (isRef() && !this.joint.isHidden){
+ @if (isRef() && !this.joint.isHidden && !this.joint.isTracer){
+ } @else if (this.joint.isTracer && !this.joint.isGrounded) {
+
+ } @else {
+
+ }
+
+ @if(isWelded()){
+
+
+
+
+
}
- @else {
-
- }
@if(isHovered() || isSelected() || showIDLabels) {
diff --git a/src/app/components/Grid/link/link.component.ts b/src/app/components/Grid/link/link.component.ts
index b3f0659c..300309fa 100644
--- a/src/app/components/Grid/link/link.component.ts
+++ b/src/app/components/Grid/link/link.component.ts
@@ -447,7 +447,36 @@ export class LinkComponent
coord = this.unitConversionService.modelCoordToSVGCoord(coord);
allCoords.push(coord);
}
- return this.svgPathService.getSingleLinkDrawnPath(allCoords, radius);
+ if (this.link.isCircle) {
+ let pathStr = '';
+ let i: number = 0;
+ let foundInput: boolean = false;
+ let inputCoords: Coord;
+ for (const [jID, currentJ] of this.link.joints) {
+ if (!foundInput && currentJ.isGrounded) {
+ inputCoords = this.unitConversionService.modelCoordToSVGCoord(currentJ.coords);
+ pathStr = this.svgPathService.getCircularLink(inputCoords, this.farthestPoint(currentJ.coords), 18 + 10);
+ foundInput = true;
+ }
+ }
+
+ return pathStr;
+
+ } else {
+ return this.svgPathService.getSingleLinkDrawnPath(allCoords, radius);
+ }
+
+ }
+
+ farthestPoint(center: Coord): number {
+ let radius: number = 0;
+ for (const [jID, currentJ] of this.link.joints) {
+ const currentR = center.getDistanceTo(currentJ.coords);
+ if (currentR > radius) {
+ radius = currentR;
+ }
+ }
+ return radius;
}
getLengthSVG() {
diff --git a/src/app/components/Grid/trajectory/trajectory.component.html b/src/app/components/Grid/trajectory/trajectory.component.html
index 31e3f3ed..1fce83e1 100644
--- a/src/app/components/Grid/trajectory/trajectory.component.html
+++ b/src/app/components/Grid/trajectory/trajectory.component.html
@@ -1,11 +1,28 @@
-
+ } @else {
+
+ }
+
diff --git a/src/app/components/Grid/trajectory/trajectory.component.ts b/src/app/components/Grid/trajectory/trajectory.component.ts
index bd36c385..205d1232 100644
--- a/src/app/components/Grid/trajectory/trajectory.component.ts
+++ b/src/app/components/Grid/trajectory/trajectory.component.ts
@@ -2,6 +2,7 @@ import { Component, Input } from '@angular/core';
import { Coord } from 'src/app/model/coord';
import { SVGPathService } from 'src/app/services/svg-path.service';
import { UnitConversionService } from 'src/app/services/unit-conversion.service';
+import {Joint} from "../../../model/joint";
@Component({
selector: '[app-trajectory]',
@@ -11,6 +12,7 @@ import { UnitConversionService } from 'src/app/services/unit-conversion.service'
export class TrajectoryComponent {
@Input() trajectory!: Coord[];
+ @Input() joint!: Joint | null;
constructor(
private svgPathService: SVGPathService,
diff --git a/src/app/components/ToolBar/undo-redo-panel/action.ts b/src/app/components/ToolBar/undo-redo-panel/action.ts
index 7d0a5076..1bef510a 100644
--- a/src/app/components/ToolBar/undo-redo-panel/action.ts
+++ b/src/app/components/ToolBar/undo-redo-panel/action.ts
@@ -24,6 +24,11 @@ export interface Action {
linkTracerData?: LinkTracerSnapshot;
linkForceData?: LinkForceSnapshot;
+ compoundLinkData?: CompoundLinkSnapshot;
+ compoundLinkDataArray?: CompoundLinkSnapshot[];
+ compoundExtraLinkData?: LinkSnapshot[];
+ compoundExtraJointsData?: JointSnapshot[][];
+
oldJointPositions?: Array<{
jointId: number;
coords: { x: number; y: number };
@@ -49,6 +54,15 @@ export interface Action {
newAngle?: number;
}
+export interface CompoundLinkSnapshot {
+ id: number;
+ linkIds: number[];
+ name: string;
+ mass: number;
+ locked: boolean;
+ color: string;
+}
+
export interface LinkSnapshot {
id: number;
jointIds: number[];
@@ -57,6 +71,7 @@ export interface LinkSnapshot {
angle: number;
locked: boolean;
color: string;
+ isCircle?: boolean;
}
export interface JointSnapshot {
@@ -72,6 +87,8 @@ export interface JointSnapshot {
locked: boolean;
isHidden: boolean;
isReference: boolean;
+ isTracer: boolean;
+ isPartOfWelded: boolean;
}
export interface LinkLockSnapshot {
@@ -94,6 +111,8 @@ export interface LinkTracerSnapshot {
linkId: number;
jointId: number;
coords: { x: number; y: number };
+ tracerModelPos?: Coord;
+ tracerSVGPos?: Coord;
}
export interface LinkForceSnapshot {
diff --git a/src/app/controllers/compound-link-interactor.ts b/src/app/controllers/compound-link-interactor.ts
index b238bd8a..cbed1c2b 100644
--- a/src/app/controllers/compound-link-interactor.ts
+++ b/src/app/controllers/compound-link-interactor.ts
@@ -6,6 +6,8 @@ import { Mechanism } from "../model/mechanism";
import { StateService } from "../services/state.service";
import { ContextMenuOption, Interactor } from "./interactor";
import { NotificationService } from "../services/notification.service";
+import type {CompoundLinkSnapshot, JointSnapshot, LinkSnapshot} from "../components/ToolBar/undo-redo-panel/action";
+import {UndoRedoService} from "../services/undo-redo.service";
/*
This interactor defines the following behaviors:
@@ -17,7 +19,7 @@ export class CompoundLinkInteractor extends Interactor {
private jointsStartPosModel: Map = new Map();
private lastNotificationTime = 0;
- constructor(public compoundLink: CompoundLink, private stateService: StateService, private notificationService: NotificationService) {
+ constructor(public compoundLink: CompoundLink, private stateService: StateService, private notificationService: NotificationService, private undoRedoService: UndoRedoService) {
super(true, true);
this.onDragStart$.subscribe(() => {
@@ -77,6 +79,7 @@ export class CompoundLinkInteractor extends Interactor {
}
const mechanism: Mechanism = this.stateService.getMechanism();
let modelPosAtRightClick = this.getMousePos().model;
+ let svgPosAtRightClick = this.getMousePos().svg;
availableContext.push(
{
icon: "assets/contextMenuIcons/addLink.svg",
@@ -87,7 +90,40 @@ export class CompoundLinkInteractor extends Interactor {
{
icon: "assets/contextMenuIcons/addTracer.svg",
label: "Attach Tracer Point",
- action: () => { mechanism.addJointToLink(this.compoundLink.id, modelPosAtRightClick) },
+ action: () => {
+ // snapshot existing joint‐IDs
+ let beforeIds: number[] = [];
+ Array.from(this.compoundLink.links.values()).forEach((l) => {
+ Array.from(l.joints.keys()).forEach((jID) => {
+ beforeIds.push(jID);
+ })
+ });
+
+ mechanism.addTracerPointWelded(this.compoundLink.id, modelPosAtRightClick, svgPosAtRightClick)
+
+ // find exactly which joint is new
+ let afterIds: number[] = [];
+ Array.from(this.compoundLink.links.values()).forEach((l) => {
+ Array.from(l.joints.keys()).forEach((jID) => {
+ afterIds.push(jID);
+ })
+ });
+ const newId = afterIds.find((id) => !beforeIds.includes(id))!;
+ const newJoint = mechanism.getJoint(newId);
+ newJoint.isTracer = true;
+
+ // recordAction with a real jointId
+ this.undoRedoService.recordAction({
+ type: 'addTracerCompound',
+ linkTracerData: {
+ linkId: this.compoundLink.id,
+ jointId: newId,
+ coords: { x: newJoint.coords.x, y: newJoint.coords.y },
+ tracerModelPos: modelPosAtRightClick,
+ tracerSVGPos: svgPosAtRightClick,
+ }
+ });
+ },
disabled: false
},
{
@@ -97,15 +133,72 @@ export class CompoundLinkInteractor extends Interactor {
disabled: false
},
{
- label: this.compoundLink.lock ? "Unlock Compound Link" : "Lock Compound Link",
+ label: this.compoundLink.lock ? "Unlock Welded Link" : "Lock Welded Link",
icon: this.compoundLink.lock ? "assets/contextMenuIcons/unlock.svg" : "assets/contextMenuIcons/lock.svg",
action: () => { this.compoundLink.lock = (!this.compoundLink.lock) },
disabled: false
},
{
icon: "assets/contextMenuIcons/trash.svg",
- label: "Delete Link",
- action: () => { mechanism.removeLink(this.compoundLink.id) },
+ label: "Delete Welded Link",
+ action: () => {
+
+ const compoundLinkData: CompoundLinkSnapshot = {
+ id: this.compoundLink.id,
+ linkIds: Array.from(this.compoundLink.links.values()).map((l) => l.id),
+ name: this.compoundLink.name,
+ mass: this.compoundLink.mass,
+ locked: this.compoundLink.lock,
+ color: this.compoundLink.color,
+ }
+
+ const linkData: LinkSnapshot[] = Array.from(
+ this.compoundLink.links.values()
+ ).map((l) => ({
+ id: l.id,
+ jointIds: Array.from(l.joints.values()).map((j) => j.id),
+ name: l.name,
+ mass: l.mass,
+ angle: l.angle,
+ locked: l.locked,
+ color: l.color,
+ isCircle: l.isCircle,
+ }));
+
+ let extraJointsData: JointSnapshot[][] = Array.from(
+ this.compoundLink.links.values()).map((l, index) => (
+ Array.from(
+ l.joints.values()
+ ).map((j) => ({
+ id: j.id,
+ coords: { x: j.coords.x, y: j.coords.y },
+ name: j.name,
+ type: j.type,
+ angle: j.angle,
+ isGrounded: j.isGrounded,
+ isWelded: j.isWelded,
+ isInput: j.isInput,
+ inputSpeed: j.speed,
+ locked: j.locked,
+ isHidden: j.hidden,
+ isReference: j.reference,
+ isTracer: j.isTracer,
+ isPartOfWelded: j.isPartOfWelded,
+ }))
+ ));
+
+ this.undoRedoService.recordAction({
+ type: "deleteCompoundLink",
+ compoundLinkData,
+ compoundExtraLinkData: linkData,
+ compoundExtraJointsData: extraJointsData,
+ });
+
+
+ mechanism.removeCompoundLinkByID(this.compoundLink.id)
+ this.stateService.getMechanism().notifyChange();
+
+ },
disabled: false
},
);
diff --git a/src/app/controllers/joint-interactor.ts b/src/app/controllers/joint-interactor.ts
index 0241a7fa..e22e99d7 100644
--- a/src/app/controllers/joint-interactor.ts
+++ b/src/app/controllers/joint-interactor.ts
@@ -5,7 +5,7 @@ import { InteractionService } from '../services/interaction.service';
import { StateService } from '../services/state.service';
import { CreateLinkFromJointCapture } from './click-capture/create-link-from-joint-capture';
import { ContextMenuOption, Interactor } from './interactor';
-import { Action } from '../components/ToolBar/undo-redo-panel/action';
+import {Action, CompoundLinkSnapshot} from '../components/ToolBar/undo-redo-panel/action';
import { NotificationService } from '../services/notification.service';
import { UndoRedoService } from '../services/undo-redo.service';
import {PositionSolverService} from "../services/kinematic-solver.service";
@@ -142,6 +142,8 @@ export class JointInteractor extends Interactor {
locked: prevJoint.locked,
isHidden: prevJoint.isHidden,
isReference: prevJoint.isReference,
+ isTracer: prevJoint.isTracer,
+ isPartOfWelded: prevJoint.isPartOfWelded,
};
let prevLinksData = connectedLinks.map((link) => {
@@ -340,6 +342,8 @@ export class JointInteractor extends Interactor {
const mechanism = this.stateService.getMechanism();
const jointToDelete = mechanism.getJoint(this.joint.id);
+
+
const jointData = {
id: jointToDelete.id,
coords: { x: jointToDelete.coords.x, y: jointToDelete.coords.y },
@@ -353,6 +357,8 @@ export class JointInteractor extends Interactor {
locked: jointToDelete.locked,
isHidden: jointToDelete.isHidden,
isReference: jointToDelete.isReference,
+ isTracer: jointToDelete.isTracer,
+ isPartOfWelded: jointToDelete.isPartOfWelded,
};
const connectedLinks =
@@ -373,6 +379,22 @@ export class JointInteractor extends Interactor {
};
});
+ const connectedWeldedLinks = mechanism.getConnectedCompoundLinks(jointToDelete);
+ const weldedlinksData: CompoundLinkSnapshot[] = connectedWeldedLinks.map((clink) => {
+ // If link.joints is a Map, convert to Array first
+ const linkIdsArray: number[] = Array.from(clink.links.values()).map(
+ (l) => l.id
+ );
+ return {
+ id: clink.id,
+ linkIds: linkIdsArray,
+ name: clink.name,
+ mass: clink.mass,
+ locked: clink.lock,
+ color: clink.color,
+ };
+ });
+
const allJointsSnapshot = mechanism
.getArrayOfJoints()
.filter((j) => j.id !== jointToDelete.id)
@@ -389,6 +411,8 @@ export class JointInteractor extends Interactor {
locked: j.locked,
isHidden: j.isHidden,
isReference: j.isReference,
+ isTracer: j.isTracer,
+ isPartOfWelded: j.isPartOfWelded,
}));
new Set(mechanism.getArrayOfJoints().map((j) => j.id));
mechanism.removeJoint(this.joint.id);
@@ -404,6 +428,7 @@ export class JointInteractor extends Interactor {
jointId: jointToDelete.id,
jointData,
linksData,
+ compoundLinkDataArray: weldedlinksData,
extraJointsData: extraJointsSnapshots,
};
@@ -426,6 +451,7 @@ export class JointInteractor extends Interactor {
// ── after ──
capture.onClick$.subscribe((mousePos) => {
const mech = this.stateService.getMechanism();
+ this.joint.isTracer = false;
// make the link (and possibly a new joint)
const hovered = capture.getHoveringJoint();
@@ -459,6 +485,8 @@ export class JointInteractor extends Interactor {
locked: newJoint.locked,
isHidden: newJoint.hidden,
isReference: newJoint.reference,
+ isTracer: newJoint.isTracer,
+ isPartOfWelded: newJoint.isPartOfWelded,
});
}
diff --git a/src/app/controllers/link-interactor.ts b/src/app/controllers/link-interactor.ts
index 84a634b3..3169ea31 100644
--- a/src/app/controllers/link-interactor.ts
+++ b/src/app/controllers/link-interactor.ts
@@ -219,6 +219,7 @@ export class LinkInteractor extends Interactor {
locked: j.locked,
isHidden: j.hidden,
isReference: j.reference,
+ isTracer: j.isTracer,
};
});
@@ -226,6 +227,8 @@ export class LinkInteractor extends Interactor {
(js) => js.coords.x === start.x && js.coords.y === start.y
)!.id;
+ mech.getJoint(attachJointId).isTracer = false;
+
this.undoRedoService.recordAction({
type: 'addLinkToLink',
parentLinkId: this.link.id,
@@ -258,6 +261,7 @@ export class LinkInteractor extends Interactor {
const afterIds = Array.from(this.link.joints.keys());
const newId = afterIds.find((id) => !beforeIds.includes(id))!;
const newJoint = this.link.joints.get(newId)!;
+ newJoint.isTracer = true;
// recordAction with a real jointId
this.undoRedoService.recordAction({
@@ -313,6 +317,7 @@ export class LinkInteractor extends Interactor {
angle: this.link.angle,
locked: this.link.locked,
color: this.link.color,
+ isCircle: this.link.isCircle,
};
const extraJointsData: JointSnapshot[] = Array.from(
@@ -330,6 +335,8 @@ export class LinkInteractor extends Interactor {
locked: j.locked,
isHidden: j.hidden,
isReference: j.reference,
+ isTracer: j.isTracer,
+ isPartOfWelded: j.isPartOfWelded,
}));
this.undoRedoService.recordAction({
@@ -345,6 +352,36 @@ export class LinkInteractor extends Interactor {
},
disabled: false,
+ },
+ {
+ icon: this.link.isCircle
+ ? 'assets/contextMenuIcons/uncircular.svg'
+ : 'assets/contextMenuIcons/circleInputLink.svg',
+ label: this.link.isCircle ? 'Make Bar' : 'Make Circular',
+ action: () => {
+ let hasGroundJoint = false;
+ for (const joint of this.link.getJoints()) {
+ if (joint.isGrounded) {
+ hasGroundJoint = true;
+ }
+ }
+
+ if (!hasGroundJoint) {
+ // prevents click if link does not have a ground joint
+ return;
+ }
+
+ this.link.isCircle = !this.link.isCircle;
+
+ this.undoRedoService.recordAction({
+ //specifies that it only needs the action name and link id
+ type: "circleLink",
+ linkId: this.link.id,
+ });
+
+ },
+ disabled:!this.link.getJoints().some(joint => joint.isGrounded),
+
}
);
}
diff --git a/src/app/controllers/svg-interactor.ts b/src/app/controllers/svg-interactor.ts
index 4cd36859..eed92661 100644
--- a/src/app/controllers/svg-interactor.ts
+++ b/src/app/controllers/svg-interactor.ts
@@ -93,7 +93,9 @@ export class SvgInteractor extends Interactor {
inputSpeed: j.speed,
locked: j.locked,
isHidden: j.hidden,
- isReference:j.reference
+ isReference:j.reference,
+ isTracer: j.isTracer,
+ isPartOfWelded: j.isPartOfWelded,
};
});
@@ -105,7 +107,8 @@ export class SvgInteractor extends Interactor {
mass: createdLink.mass,
angle: createdLink.angle,
locked: createdLink.locked,
- color: createdLink.color
+ color: createdLink.color,
+ isCircle: createdLink.isCircle,
};
this.undoRedoService.recordAction({
diff --git a/src/app/model/joint.ts b/src/app/model/joint.ts
index c4282c95..da8398ea 100644
--- a/src/app/model/joint.ts
+++ b/src/app/model/joint.ts
@@ -23,6 +23,8 @@ export class Joint {
private _isReference: boolean;
private _isGenerated: boolean;
private _rpmSpeed: number;
+ private _isTracer: boolean = false;
+ private _isPartOfWelded: boolean = false;
isInput$ = this._isInput.asObservable();
isGrounded$ = this._isGrounded.asObservable();
@@ -119,6 +121,13 @@ export class Joint {
get isGenerated(): boolean {
return this._isGenerated;
}
+ get isTracer(): boolean {
+ return this._isTracer;
+ }
+
+ get isPartOfWelded(): boolean {
+ return this._isPartOfWelded;
+ }
getInputObservable() {
return this.isInput$;
@@ -176,6 +185,13 @@ export class Joint {
set generated(val: boolean) {
this._isGenerated = val;
}
+ set isTracer(val: boolean) {
+ this._isTracer = val;
+ }
+
+ set isPartOfWelded(val: boolean) {
+ this._isPartOfWelded = val;
+ }
//----------------------------Joint Modification with modifying other variables----------------------------
addGround() {
diff --git a/src/app/model/link.ts b/src/app/model/link.ts
index 790a2879..9317177c 100644
--- a/src/app/model/link.ts
+++ b/src/app/model/link.ts
@@ -1,6 +1,8 @@
import { Coord } from './coord';
import { Joint } from './joint';
import { Force } from './force';
+import {UnitConversionService} from "../services/unit-conversion.service";
+import {SVGPathService} from "../services/svg-path.service";
export interface RigidBody {
getJoints(): Joint[];
@@ -17,6 +19,7 @@ export class Link implements RigidBody {
private _color: string = '';
private _isLocked: boolean;
private _angle: number;
+ private _isCircle: boolean = false;
// These fields are only for supporting CUSTOM centerOfMass logic
private _customCOMAnchors: [number, number] | null = null; // We store IDs so we can find the correct joints later even if we have references.
@@ -45,7 +48,7 @@ export class Link implements RigidBody {
this._color = this.linkColorOptions[id % this.linkColorOptions.length];
this._isLocked = false;
this._angle = 0;
-
+
if (Array.isArray(jointAORJoints)) {
jointAORJoints.forEach((joint) => {
this._joints.set(joint.id, joint);
@@ -117,6 +120,10 @@ export class Link implements RigidBody {
return parseFloat(posangle.toFixed(3));
}
+ get isCircle(): boolean {
+ return this._isCircle;
+ }
+
//setters
set name(value: string) {
this._name = value;
@@ -138,6 +145,10 @@ export class Link implements RigidBody {
this._angle = ((value % 360) + 360) % 360;
}
+ set isCircle(value: boolean) {
+ this._isCircle = value;
+ }
+
// In the future, we need to check if this custom center of mass is valid.
setCenterOfMass(newX: number, newY: number) {
this._centerOfMass.x = newX;
@@ -183,7 +194,7 @@ export class Link implements RigidBody {
this._customCOMAnchors = null;
this._customCOMRadii = null;
this._centerOfMass = this.calculateCenterOfMass();
-
+
// Rebind forces to the new COM definition
this.rebindForcesAfterCenterOfMassChanged();
}
@@ -262,7 +273,7 @@ export class Link implements RigidBody {
}
// MQP 24-25: I don't think this works
- // MQP 25-26: No, this doesn't work for all shapes, it only works if we have solid filled nice shapes
+ // MQP 25-26: No, this doesn't work for all shapes, it only works if we have solid filled nice shapes
// like triangle, square,... Hope that another MQP can find a better way to calculate it.
calculateCenterOfMass(): Coord {
@@ -270,20 +281,20 @@ export class Link implements RigidBody {
if (!this._customCenterOfMass) {
this._centerOfMass = this.calculateCOMUsingAverageMethod();
}
-
+
// if using CUSTOM
const customCOM = this.calculateCOMUsingCircleMethod();
if (!customCOM) { // if circle method fails, fallback to avarage method
this._centerOfMass = this.calculateCOMUsingAverageMethod();
-
+
if (!this._COMNeedReset) { // need to reset everything related to custom Center of mass
this._customCenterOfMass = false;
this._customCOMAnchors = null;
this._customCOMRadii = null;
- }
+ }
}
-
+
return this._centerOfMass;
}
@@ -358,25 +369,25 @@ export class Link implements RigidBody {
if (d < eps && Math.abs(r0 - r1) < eps) {
console.log('two circle centers are almost the same');
return [];
- }
+ }
// same center with different radius
if (d < eps) {
console.log('same center with different radius')
- return [];
- }
+ return [];
+ }
// Circles too far apart
if (d > r0 + r1 + eps) {
console.log('Circles too far apart');
return [];
- }
+ }
// one circle inside the other without touching
if (d < Math.abs(r0 - r1) - eps) {
console.log('one circle inside the other without touching');
return [];
- }
+ }
// Compute intersection(s), reading this article at section "Intersection of two circles" https://paulbourke.net/geometry/circlesphere/
const a = (r0 * r0 - r1 * r1 + d * d) / (2 * d);
@@ -402,11 +413,11 @@ export class Link implements RigidBody {
// console.log('1 intersection between circles');
return [new Coord(point2_x, point2_y)];
}
-
+
const p1 = new Coord(point2_x - vx, point2_y + vy);
const p2 = new Coord(point2_x + vx, point2_y - vy);
return [p1, p2];
- }
+ }
getMidpoint(joint1: Joint, joint2: Joint): Coord {
let x: number;
@@ -627,6 +638,42 @@ export class Link implements RigidBody {
return false;
}
}
+ // checks if coord is in link. Assumes coords have been converted into svg coords from model coords
+ containsCoord(coord: Coord): boolean {
+ // below is getting the path string of this link
+ let joints: IterableIterator = this.joints.values();
+ let allCoords: Coord[] = [];
+ let unitConversionService: UnitConversionService = new UnitConversionService();
+ for (let joint of joints) {
+ let coord: Coord = joint._coords;
+ coord = unitConversionService.modelCoordToSVGCoord(coord);
+ allCoords.push(coord);
+ }
+ const pathString = new SVGPathService(unitConversionService).getSingleLinkDrawnPath(allCoords, 30);
+
+ // NS below is used to create an SVGPathElement instead of an unknown HTML element, by using namespace
+ const NS = "http://www.w3.org/2000/svg";
+ const path = document.createElementNS(NS, "path");
+
+ // creating path element from path string that is invisible
+ path.setAttribute("d", pathString);
+ path.setAttribute("fill", "black");
+
+ // checking that svg exists before calculating for whether the coordinate is within the path or not
+ const svg = document.querySelector('svg');
+ if (svg != null) {
+ path.setAttribute("visibility", "hidden");
+ svg?.appendChild(path);
+
+ // check if coord is within the path or not
+ let pointTwo = new DOMPoint(coord.x, coord.y);
+ const isInPath = path.isPointInFill(pointTwo);
+ path.remove();
+ return isInPath;
+ }
+ return false;
+ }
+
moveCoordinates(coord: Coord) {
for (const jointID of this._joints.keys()) {
const joint = this._joints.get(jointID)!;
diff --git a/src/app/model/mechanism.ts b/src/app/model/mechanism.ts
index c66d77e2..d5e2481a 100644
--- a/src/app/model/mechanism.ts
+++ b/src/app/model/mechanism.ts
@@ -725,13 +725,17 @@ export class Mechanism {
*
* @param {number} linkID
* @param {Coord} coord
+ * @param {isTracer} boolean
* @memberof Mechanism
*/
- addJointToLink(linkID: number, coord: Coord) {
+ addJointToLink(linkID: number, coord: Coord, isTracer?: boolean) {
this.executeLinkAction(linkID, (link) => {
let jointA = new Joint(this._jointIDCount, coord);
this._jointIDCount++;
this._joints.set(jointA.id, jointA);
+ if (isTracer) {
+ jointA.isTracer = true;
+ }
link.addTracer(jointA);
});
}
@@ -752,6 +756,95 @@ export class Mechanism {
});
}
+ /**
+ * attaches a tracer point(effectively a joint) to an existing link, but this is from a welded link.
+ *
+ * @param {number} linkID
+ * @param {Coord} coord
+ * @memberof Mechanism
+ */
+ addTracerPointWelded(linkID: number, coordModel: Coord, coordSVG: Coord) {
+ this.executeLinkAction(linkID, (link) => {
+ let jointA = new Joint(this._jointIDCount, coordModel);
+ this._jointIDCount++;
+ if (this.isMechanismWelded()) {
+ jointA.isTracer = true;
+ jointA.isPartOfWelded = true;
+ }
+
+ let foundLink: boolean = false;
+ let i: number = 0;
+ while (i < this._links.size && !foundLink) {
+ let currentLink = this._links.get(i);
+ const inLink = currentLink?.containsCoord(coordSVG);
+
+ if (inLink) {
+ foundLink = true;
+ } else {
+ i++;
+ }
+ }
+
+ this._joints.set(jointA.id, jointA);
+
+ const theLink = this._links.get(i);
+ if (foundLink && theLink != undefined) {
+ theLink.addTracer(jointA);
+ } else {
+ link.addTracer(jointA);
+ }
+ });
+ }
+
+ /**
+ * attaches a tracer point(effectively a joint) to an existing link, but this is from a welded link.
+ *
+ * @param {number} linkID
+ * @param {Coord} coord
+ * @memberof Mechanism
+ */
+ _addJointToWelded(linkID: number, jointA: Joint, coordSVG: Coord) {
+ this.executeLinkAction(linkID, (link) => {
+
+ let foundLink: boolean = false;
+ let i: number = 0;
+ while (i < this._links.size && !foundLink) {
+ let currentLink = this._links.get(i);
+ const inLink = currentLink?.containsCoord(coordSVG);
+
+ if (inLink) {
+ foundLink = true;
+ } else {
+ i++;
+ }
+ }
+
+ this._joints.set(jointA.id, jointA);
+
+ const theLink = this._links.get(i);
+ if (foundLink && theLink != undefined) {
+ theLink.addTracer(jointA);
+ } else {
+ link.addTracer(jointA);
+ }
+ });
+ }
+
+ /**
+ * determines whether the current mechanism is a welded linkage.
+ *
+ * @memberof Mechanism
+ * @return returns true if the mechanism has a welded joint, false otherwise
+ */
+ isMechanismWelded(): boolean {
+ for (const [id, j] of this._joints) {
+ if (j.isWelded) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* attaches a new link to another link at a point along the existing link which is not a joint.
*
@@ -882,6 +975,18 @@ export class Mechanism {
}
}
+ // first removes every link within the compound link, then the compound link itself; finds compound link from id
+ public removeCompoundLinkByID(compoundLinkID: number) {
+ var compoundLink = this._compoundLinks.get(compoundLinkID);
+ if (compoundLink != undefined) {
+ for (let link of compoundLink.links.values()) {
+ this.removeLink(link.id);
+ }
+ this._compoundLinks.delete(compoundLink.id);
+ }
+
+ }
+
// first removes every link within the compound link, then the compound link itself
public removeCompoundLink(compoundLink: CompoundLink) {
for (let link of compoundLink.links.values()) {
@@ -1129,7 +1234,7 @@ export class Mechanism {
}
return connectedLinks;
}
- private getConnectedCompoundLinks(joint: Joint): CompoundLink[] {
+ getConnectedCompoundLinks(joint: Joint): CompoundLink[] {
let connectedCompoundLinks: CompoundLink[] = [];
for (let link of this._compoundLinks.values()) {
if (link.containsJoint(joint.id)) {
diff --git a/src/app/services/decoder.service.ts b/src/app/services/decoder.service.ts
index cb529289..5e0c6cbb 100644
--- a/src/app/services/decoder.service.ts
+++ b/src/app/services/decoder.service.ts
@@ -98,6 +98,8 @@ export class DecoderService {
}
// Pass the reconstructed mechanism data to the state service.
+ console.log('fullData: Compound Link');
+ console.log(fullData.decodedCompoundLinks);
stateService.reconstructMechanism(fullData);
if (compactData.u) {
@@ -210,6 +212,8 @@ export class DecoderService {
isHidden: this.convertBoolean(row[11]),
isReference: this.convertBoolean(row[12]),
isGenerated: this.convertBoolean(row[13]),
+ isPartOfWelded: this.convertBoolean(row[14]),
+ isTracer: this.convertBoolean(row[15]),
}));
const decodedLinks: any[] = (compactData.l || []).map((row: any[]) => ({
@@ -224,6 +228,7 @@ export class DecoderService {
locked: this.convertBoolean(row[8]),
length: this.convertNumber(row[9], useDecoding),
angle: this.convertNumber(row[10], useDecoding),
+ isCircle: this.convertBoolean(row[11]),
}));
const decodedCompoundLinks: any[] = (compactData.c || []).map(
diff --git a/src/app/services/encoder.service.ts b/src/app/services/encoder.service.ts
index 1f4b7557..0161618f 100644
--- a/src/app/services/encoder.service.ts
+++ b/src/app/services/encoder.service.ts
@@ -195,6 +195,8 @@ export class EncoderService {
j.isHidden,
j.isReference,
j.isGenerated,
+ j.isPartOfWelded,
+ j.isTracer,
]),
l: mechanism.getArrayOfLinks().map((l: Link) => [
l.id,
@@ -212,6 +214,7 @@ export class EncoderService {
l.locked,
l.length,
l.angle,
+ l.isCircle,
]),
c: mechanism.getArrayOfCompoundLinks().map((cl: CompoundLink) => [
cl.id,
diff --git a/src/app/services/state.service.ts b/src/app/services/state.service.ts
index 9ae5f47e..cb3aa983 100644
--- a/src/app/services/state.service.ts
+++ b/src/app/services/state.service.ts
@@ -119,6 +119,8 @@ export class StateService {
newJoint.hidden = Boolean(joint.isHidden);
newJoint.reference = Boolean(joint.isReference);
newJoint.generated = Boolean(joint.isGenerated);
+ newJoint.isTracer = Boolean(joint.isTracer);
+ newJoint.isPartOfWelded = Boolean(joint.isPartOfWelded);
if (Boolean(joint.isInput)) {
newJoint.addInput();
}
@@ -145,13 +147,14 @@ export class StateService {
for (const x of jointsArray) {
console.log(x.id);
}
- console.log(link, link.id);
- let newLink = new Link(link.id, jointsArray);
+ console.log(link, Number(link.id));
+ let newLink = new Link(Number(link.id), jointsArray);
newLink.name = link.name;
newLink.mass = link.mass;
newLink.angle = link.angle;
newLink.locked = Boolean(link.locked);
newLink.color = link.color;
+ newLink.isCircle = Boolean(link.isCircle);
this.mechanism._addLink(newLink);
}
@@ -159,6 +162,7 @@ export class StateService {
//Compound Links
if (rawData.decodedCompoundLinks) {
+ console.log("WELDED LINKS")
for (const compoundlink of rawData.decodedCompoundLinks) {
let linksArray: Link[] = compoundlink.links
.split('|')
diff --git a/src/app/services/svg-path.service.ts b/src/app/services/svg-path.service.ts
index 19a0fec9..dadb1788 100644
--- a/src/app/services/svg-path.service.ts
+++ b/src/app/services/svg-path.service.ts
@@ -1,12 +1,18 @@
import {Injectable} from '@angular/core';
import {Coord} from 'src/app/model/coord'
+import {Link} from "../model/link";
+import {UnitConversionService} from "./unit-conversion.service";
+import {arc} from "d3-shape";
+import {intersection} from "d3";
@Injectable({
providedIn: 'root',
})
export class SVGPathService {
- constructor() {}
+ private scale = 1;
+
+ constructor(private unitConversionService: UnitConversionService) {};
// Generates an SVG path string representing a single connection based on provided coordinates and radius.
getSingleLinkDrawnPath(allCoords: Coord[], radius: number): string{
@@ -23,6 +29,19 @@ export class SVGPathService {
return this.calculateConvexPath(hullCoords,radius);
}
+ // draws the circular link for an input link
+ // radius should be positive and inputCoords should already be converted to SVG
+ // offset is accounting for joint radius
+ getCircularLink(inputCoords: Coord, radius: number, offset: number): string {
+ let pathData = '';
+ const convertedRadius = this.unitConversionService.modelNumToSVGNum(radius) + offset;
+ pathData += `M ${inputCoords.x - convertedRadius},${inputCoords.y} `; // moving to center of the circle
+ pathData += `A ${convertedRadius},${convertedRadius} 0 1 0 ${inputCoords.x + convertedRadius},${inputCoords.y} `; // drawing first semicircle
+ pathData += `A ${convertedRadius},${convertedRadius} 0 1 0 ${inputCoords.x - convertedRadius},${inputCoords.y} Z`; // drawing second semicircle
+
+ return pathData;
+ }
+
// Creates an SVG path string tracing the trajectory through all provided coordinates with the given radius.
getTrajectoryDrawnPath(allCoords: Coord[], radius: number): string {
if (allCoords.length === 0) {
@@ -53,6 +72,7 @@ export class SVGPathService {
return this.calculateConvexPath(hullCoords,radius);
}
+
// Identifies if all coordinates lie on a straight line and returns the endpoints if they are collinear.
private findCollinearCoords(coords: Coord[]): Coord[] | undefined {
@@ -251,4 +271,962 @@ export class SVGPathService {
return pathData;
}
+ // checks if the path string is starting or ending a path; returns true if it is
+ isNewShape(pathString: string): boolean {
+ return pathString === '' || pathString.substring(pathString.length - 2) === 'Z ';
+ }
+
+ // used for creating a concave fillet between two lines
+ computeArcPointsAndRadius(line1: [Coord, Coord, Coord | null, Link], line2:[Coord, Coord, Coord | null, Link], arcRadius: number):[Coord, Coord, number] {
+ // modify line1 end point and line2 start point to create an arc between them
+ const arcOffset = Math.min(arcRadius, line1[0].getDistanceTo(line1[1]) / 2, line2[0].getDistanceTo(line2[1]) / 2);
+ const line1OffsetPoint:Coord = line1[0].clone()
+ .subtract(line1[1])
+ .normalize()
+ .multiply(arcOffset)
+ .add(line1[1]);
+
+ const line2OffsetPoint:Coord = line2[1].clone()
+ .subtract(line2[0])
+ .normalize()
+ .multiply(arcOffset)
+ .add(line2[0]);
+
+ // find angle between two lines
+ const line2Angle = Math.atan2(line2[1].y - line2[0].y, line2[1].x - line2[0].x);
+ const line1Angle = Math.atan2(line1[1].y - line1[0].y, line1[1].x - line1[0].x);
+ const angleBetweenLines = line2Angle - line1Angle;
+ const radius = arcOffset * Math.tan((Math.PI - angleBetweenLines) / 2);
+
+ return [line1OffsetPoint, line2OffsetPoint, radius];
+ }
+
+ // recalculates angle to be in between -pi and pi.
+ angleToPI(line1: [Coord, Coord, Coord | null, Link], line2:[Coord, Coord, Coord | null, Link]) {
+ // calculate angle of each individual line, given in radians
+ let line1Angle = Math.atan2(line1[1].y - line1[0].y, line1[1].x - line1[0].x);
+ let line2Angle = Math.atan2(line2[1].y - line2[0].y, line2[1].x - line2[0].x);
+
+ // angle in between the lines
+ let angle = line1Angle - line2Angle;
+
+ // recalculate the angle to be between -pi and pi
+ angle = angle % (2 * Math.PI);
+ if (angle > Math.PI) {
+ angle -= 2 * Math.PI;
+ }
+ if (angle < -Math.PI) {
+ angle += 2 * Math.PI;
+ }
+ return angle;
+ }
+
+ // checks if two numbers are loosely equal, bounded by a delta
+ twoNumsLooselyEquals(a: number, b: number, delta: number = 0.00001): boolean {
+ return Math.abs(a - b) <= delta;
+ }
+
+ // checks if a point is on a line
+ // return true if the point is on the line segment defined by lineStart and lineEnd
+ // return false otherwise
+ isPointOnLine(point: Coord, lineStart: Coord, lineEnd: Coord, delta: number = 0.000001): boolean {
+ // check if the point is within a small range of the line segment.
+ let range = 0.00001;
+ if (
+ point.x < Math.min(lineStart.x, lineEnd.x) - range ||
+ point.x > Math.max(lineStart.x, lineEnd.x) + range ||
+ point.y < Math.min(lineStart.y, lineEnd.y) - range ||
+ point.y > Math.max(lineStart.y, lineEnd.y) + range
+ ) {
+ return false;
+ }
+
+ // check if three points are collinear
+ const crossProduct =
+ (point.x - lineStart.x) * (lineEnd.y - lineStart.y) -
+ (point.y - lineStart.y) * (lineEnd.x - lineStart.x);
+
+ if (Math.abs(crossProduct) > delta) {
+ return false;
+ }
+
+ // point is collinear and in between the range
+ return true;
+ }
+
+ // checks if a point is on an arc
+ // return true if the point is within the arc
+ // return false if the point is outside the circle or if the point is on the circle, but not within the arc
+ isPointInArc(intersection: Coord, arcStart: Coord, arcEnd: Coord, arcCenter: Coord, radius: number, delta: number = 0.0001): boolean {
+ // the arc always goes from the start point to the end point in a clockwise direction.
+ const dx = intersection.x - arcCenter.x;
+ const dy = intersection.y - arcCenter.y;
+
+ const dist = Math.sqrt(dx * dx + dy * dy);
+
+ // is point on the circle the arc is on?
+ if (Math.abs(dist - radius) > delta) {
+ return false;
+ }
+
+ // use cross product to determine whether the point is within the arc
+ const crossProduct1 =
+ (arcStart.x - arcCenter.x) * (intersection.y - arcCenter.y) -
+ (arcStart.y - arcCenter.y) * (intersection.x - arcCenter.x);
+ const crossProduct2 =
+ (intersection.x - arcCenter.x) * (arcEnd.y - arcCenter.y) -
+ (intersection.y - arcCenter.y) * (arcEnd.x - arcCenter.x);
+
+ // if the crossProducts are both greater than negative delta, then the point is within the arc
+ return (crossProduct1 >= -delta && crossProduct2 >= -delta);
+ }
+
+ // https://stackoverflow.com/questions/13937782/calculating-the-point-of-intersection-of-two-lines
+ // line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/
+ // Determine the intersection point of two line segments
+ // Return undefined if the lines don't intersect
+ lineLineIntersect(line1Start: Coord, line1End: Coord, line2Start: Coord, line2End: Coord): Coord[] | undefined {
+ let x1 = line1Start.x;
+ let y1 = line1Start.y;
+ let x2 = line1End.x;
+ let y2 = line1End.y;
+ let x3 = line2Start.x;
+ let y3 = line2Start.y;
+ let x4 = line2End.x;
+ let y4 = line2End.y;
+
+ let delta = 0
+
+ // check if none of the lines are of length 0
+ if ((this.twoNumsLooselyEquals(x1, x2) && this.twoNumsLooselyEquals(y1, y2)) || (this.twoNumsLooselyEquals(x3, x4) && this.twoNumsLooselyEquals(y3, y4))) {
+ return undefined;
+ }
+
+ let denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
+
+ // check if lines are parallel
+ if (denominator === 0) {
+ return undefined;
+ }
+
+ let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
+ let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
+
+ // check if intersection is not within the segments
+ if (ua <= 0 || ua >= 1 || ub <= 0 || ub >= 1) {
+ return undefined;
+ }
+
+ // return Coord with x and y coordinates of intersection
+ let x = x1 + ua * (x2 - x1);
+ let y = y1 + ua * (y2 - y1);
+
+ let intersection = new Coord(x, y);
+
+ // checks if the intersection is an end point of the segments
+ if (intersection.equals(line1Start, this.scale) ||
+ intersection.equals(line1End, this.scale) ||
+ intersection.equals(line2Start, this.scale) ||
+ intersection.equals(line2End, this.scale)) {
+ return undefined;
+ }
+
+ return [intersection];
+ }
+
+ // return the intersection points between a line and a circle.
+ // if the line is tangent to the circle, then return the point of tangency.
+ // if the line does not intersect the circle, return undefined.
+ lineCircleIntersect(lineStart: Coord, lineEnd: Coord, circleCenter: Coord, circleRadius: number): Coord[] | undefined {
+ // check if the line is vertical or not
+ if (this.twoNumsLooselyEquals(lineEnd.x, lineStart.x)) {
+ // line is vertical so equation is x = c
+ let c = lineStart.x;
+
+ // find equation of the circle in the form (x - h)^2 + (y - k)^2 = r^2
+ let h = circleCenter.x;
+ let k = circleCenter.y;
+ let r = circleRadius;
+
+ // substitute x = c into the circle equation and solve for y
+ // this will give a quadratic equation in the form ay^2 + by + c = 0
+ let a = 1; // coefficient of y^2
+ let b = -2 * k; // coefficient of y
+ let d = c - h; // constant term divided by 2
+ let e = d * d + k * k - r * r; // constant term
+
+ // find the discriminant of the quadratic equation
+ // this will determine how many solutions there are
+ let D = b * b - 4 * a * e; // discriminant
+
+
+ if (D < 0) {
+ // if D is negative, then there are no real solutions and the line does not intersect the circle
+ return undefined;
+ } else if (D === 0) {
+ // if D is zero, then there is one real solution and the line is tangent to the circle
+ let y = -b / (2 * a); // solution for y
+ return [new Coord(c, y)]; // return the point of tangency as an array of one coordinate object
+ } else if (D > 0) {
+ // if D is positive, then there are two real solutions and the line intersects the circle at two points
+ let y1 = (-b + Math.sqrt(D)) / (2 * a); // first solution for y
+ let y2 = (-b - Math.sqrt(D)) / (2 * a); // second solution for y
+ return [new Coord(c, y1), new Coord(c, y2)]; // return the intersection points as an array of two coordinate objects
+ }
+ } else {
+ let intersections: Coord[] = [];
+ let slope = (lineStart.y - lineEnd.y) / (lineStart.x - lineEnd.x);
+ let y_intercept = lineStart.y - slope * lineStart.x;
+
+ let a = 1 + slope * slope;
+ let b = 2 * slope * (y_intercept - circleCenter.y) - 2 * circleCenter.x;
+ let c =
+ circleCenter.x * circleCenter.x +
+ (y_intercept - circleCenter.y) * (y_intercept - circleCenter.y) -
+ circleRadius * circleRadius;
+
+ let discriminant = b * b - 4 * a * c;
+ const tolerance = 0.00001;
+ if (discriminant < -tolerance) {
+ // line doesn't touch circle
+ return undefined;
+ } else if (discriminant < tolerance) {
+ // line is tangent to circle
+ let x = -b / (2 * a);
+ let y = slope * x + y_intercept;
+ intersections.push(new Coord(x, y));
+ return intersections;
+ } else {
+ // line intersects circle in two places
+ let x1 = (-b + Math.sqrt(discriminant)) / (2 * a);
+ let y1 = slope * x1 + y_intercept;
+ intersections.push(new Coord(x1, y1));
+ let x2 = (-b - Math.sqrt(discriminant)) / (2 * a);
+ let y2 = slope * x2 + y_intercept;
+ intersections.push(new Coord(x2, y2));
+ return intersections;
+ }
+ }
+ return undefined;
+ }
+
+ // return the first intersection point between a line and an arc. The first is the one closest to the lineStart point.
+ // if the line is tangent to the arc, then return the point of tangency closest to the lineStart point.
+ // if the line does not intersect the arc, return undefined.
+ lineArcIntersect(lineStart: Coord, lineEnd: Coord, arcStart: Coord, arcEnd: Coord, arcCenter: Coord, findIntersectionCloseTo: Coord, arcRadius: number): Coord[] | undefined {
+ // find the intersection points between the line and the circle defined by the arc
+ let intersections: Coord[] | undefined = this.lineCircleIntersect(lineStart, lineEnd, arcCenter, arcRadius);
+
+ intersections = intersections?.filter((intersection) => {
+ return this.isPointOnLine(intersection, lineStart, lineEnd);
+ });
+
+ if (intersections === undefined || intersections.length === 0) {
+ return undefined;
+ }
+
+ // check if the intersection points are within the arc
+ let returnIntersection: Coord[] = [];
+ for (let intersection of intersections) {
+ if (
+ this.isPointInArc(intersection, arcStart, arcEnd, arcCenter, arcRadius)
+ ) {
+ returnIntersection.push(intersection);
+ }
+ }
+ return returnIntersection;
+ }
+
+ // return the intersection points between two circles.
+ // if the circles do not intersect, return undefined.
+ circleCircleIntersect(center: Coord, radius: number, center2: Coord, radius2: number): [Coord[] | undefined, boolean] {
+ let x1 = center.x;
+ let y1 = center.y;
+ let x2 = center2.x;
+ let y2 = center2.y;
+
+ let dx = x2 - x1;
+ let dy = y2 - y1;
+ let d = Math.sqrt(dx * dx + dy * dy);
+
+ // Circles are separate
+ if (d > radius + radius2) {
+ return [undefined, false];
+ }
+
+ // One circle is contained within the other
+ if (d < Math.abs(radius - radius2)) {
+ return [undefined, false];
+ }
+
+ // Circles are coincident
+ if (d === 0 && radius === radius2) {
+ return [undefined, true];
+ }
+
+ // calculate intersection points
+ let a = (radius * radius - radius2 * radius2 + d * d) / (2 * d);
+ let h = Math.sqrt(radius * radius - a * a);
+ let x3 = x1 + (a * dx) / d;
+ let y3 = y1 + (a * dy) / d;
+ let x4 = x3 + (h * dy) / d;
+ let y4 = y3 - (h * dx) / d;
+ let x5 = x3 - (h * dy) / d;
+ let y5 = y3 + (h * dx) / d;
+
+ return [[new Coord(x4, y4), new Coord(x5, y5)], false];
+ }
+
+ // return the first intersection point between two arcs, the one closest to the startPosition point
+ // if the arcs are tangent, then return the point of tangency closest to the startPosition point
+ // if the arcs do not intersect, return undefined
+ arcArcIntersect(startPosition: Coord, endPosition: Coord, center: Coord, startPosition2: Coord, endPosition2: Coord, center2: Coord, radius: number): Coord[] | undefined {
+ // find the intersection points between the two circles defined by the arcs
+ let [intersections, coincident]: [Coord[] | undefined, boolean] = this.circleCircleIntersect(center, radius, center2, radius);
+
+ if (coincident) {
+ // circles are coincident, so we need to check if the arcs intersect
+ // check if the start and end points of the arcs are within the other arc
+
+ let allImportantIntersections: Coord[] = [];
+
+ if (this.isPointInArc(startPosition, startPosition2, endPosition2, center, radius)) {
+ allImportantIntersections.push(startPosition);
+ }
+ if (this.isPointInArc(endPosition, startPosition2, endPosition2, center, radius)) {
+ allImportantIntersections.push(endPosition);
+ }
+ if (this.isPointInArc(startPosition2, startPosition, endPosition, center, radius)) {
+ allImportantIntersections.push(startPosition2);
+ }
+ if (this.isPointInArc(endPosition2, startPosition, endPosition, center, radius)) {
+ allImportantIntersections.push(endPosition2);
+ }
+
+ // if there are no intersections, then the arcs do not intersect.
+ if (allImportantIntersections.length === 0) {
+ return undefined;
+ }
+
+ return allImportantIntersections;
+ }
+
+ // check if no intersections
+ if (intersections === undefined) {
+ return undefined;
+ }
+
+ // check if intersection points are within the arcs
+ let closestIntersection: Coord | undefined;
+
+ for (let intersection of intersections) {
+ if (
+ this.isPointInArc(intersection, startPosition, endPosition, center, radius) &&
+ this.isPointInArc(intersection, startPosition2, endPosition2, center2, radius) &&
+ !intersection.equals(startPosition, this.scale) &&
+ !intersection.equals(endPosition, this.scale) &&
+ !intersection.equals(startPosition2, this.scale) &&
+ !intersection.equals(endPosition2, this.scale)
+ ) {
+ if (!closestIntersection) {
+ // for first iteration only
+ closestIntersection = intersection;
+ } else if (intersection.getDistanceTo(startPosition) < closestIntersection.getDistanceTo(startPosition)) {
+ closestIntersection = intersection;
+ }
+ }
+ }
+
+ if (closestIntersection === undefined) {
+ return undefined;
+ } else {
+ return [closestIntersection];
+ }
+ }
+
+ // finds intersection between line/arc and line/arc
+ intersectsWith(line1: [Coord, Coord, Coord | null, Link], line2: [Coord, Coord, Coord | null, Link], radius: number): Coord[] | undefined {
+ if (line1[2] === null && line2[2] === null) {
+ // line and line intersection
+ return this.lineLineIntersect(line1[0], line1[1], line2[0], line2[1]);
+ } else if (line1[2] === null && line2[2] !== null) {
+ // line and arc intersection
+ return this.lineArcIntersect(line1[0], line1[1], line2[0], line2[1], line2[2], line1[0], radius);
+ } else if (line1[2] !== null && line2[2] === null) {
+ // arc and line intersection
+ return this.lineArcIntersect(line2[0], line2[1], line1[0], line1[1], line1[2], line1[0], radius);
+ } else if (line1[2] !== null && line2[2] !== null) {
+ // arc and arc intersection
+ return this.arcArcIntersect(line1[0], line1[1], line1[2], line2[0], line2[1], line2[2], radius);
+ } else {
+ // error
+ return undefined;
+ }
+ }
+
+ // splits the intersections by which lines each one was in
+ groupIntersectionsByPath(lines: [Coord, Coord, Coord | null, Link][], intersections: {point: Coord, i: number}[]): Coord[][] {
+ return lines.map((_, index) => {
+ return intersections.filter(p => p.i === index)
+ .map(p => (p.point));
+ });
+ }
+
+ // calculates projection of a point on a line
+ projectPointOnLine(p: Coord, lineStart: Coord, lineEnd: Coord) {
+ const startEndX = lineEnd.x - lineStart.x;
+ const startEndY = lineEnd.y - lineStart.y;
+
+ const startPX = p.x - lineStart.x;
+ const startPY = p.y - lineStart.y;
+
+ const startEndLenSq = startEndX * startEndX + startEndY * startEndY;
+ // is normalized, so between 0 and 1
+ return (startPX * startEndX + startPY * startEndY) / startEndLenSq;
+ }
+
+ // split a line by the intersection points
+ splitLineByIntersections(line: [Coord, Coord, Coord | null, Link], points: Coord[]): [Coord, Coord, Coord | null, Link][] {
+ // calculate the projection and filter out any points with t below 0 or above 1
+ let tPoints = points.map(point => {
+ let t = this.projectPointOnLine(point, line[0], line[1]);
+ return {p: point, t: t};
+ }).filter(point => {
+ return point.t > 0 && point.t < 1;
+ });
+
+ // sort according to t, the projection
+ tPoints.sort((a, b) => {
+ return a.t - b.t;
+ });
+
+ // add endpoints
+ const orderedPoints = [{p: line[0], t: 0}, ...tPoints, {p: line[1], t: 1}];
+
+ // rebuild line segments
+ let segments: [Coord, Coord, Coord | null, Link][] = [];
+ for (let i = 0; i < orderedPoints.length - 1; i++) {
+ const pointOne = orderedPoints[i];
+ const pointTwo = orderedPoints[i + 1];
+ segments.push([pointOne.p, pointTwo.p, line[2], line[3]]);
+ }
+ return segments;
+ }
+
+ // split an arc along intersection points
+ splitArcByIntersection(arc: [Coord, Coord, Coord | null, Link], points: Coord[]): [Coord, Coord, Coord | null, Link][] {
+ if (arc[2] !== null) {
+ // getting angles of every point from the center of the arc
+ let intersectionsWithAngles:{p:Coord, angle: number}[] = points.map((p) => ({
+ p: p,
+ angle: Math.atan2(p.y - (arc[2]?.y ?? 0), p.x - (arc[2]?.x ?? 0))
+ }));
+
+ // change range from [0 to 2pi] and compare angle distance from start angle of arc
+ const twoPI = 2 * Math.PI;
+ // making sure arc angle is between [0, 2pi]
+ const startArcAngle = (Math.atan2(arc[0].y - (arc[2]?.y ?? 0), arc[0].x - (arc[2]?.x ?? 0)) % twoPI + twoPI) % twoPI;
+ let resultOrderedPoints = intersectionsWithAngles.sort((a, b) => {
+ const aFromStart = (a.angle % twoPI + twoPI) % twoPI - startArcAngle;
+ const bFromStart = (b.angle % twoPI + twoPI) % twoPI - startArcAngle;
+
+ const posAAngle = (aFromStart + twoPI) % twoPI;
+ const posBAngle = (bFromStart + twoPI) % twoPI;
+ return posAAngle - posBAngle;
+ });
+
+ // removes any intersection points that are at the end points
+ resultOrderedPoints = resultOrderedPoints.filter((point) => {
+ return !((this.twoNumsLooselyEquals(point.p.x, arc[0].x) && this.twoNumsLooselyEquals(point.p.y, arc[0].y)) || (this.twoNumsLooselyEquals(point.p.x, arc[1].x) && this.twoNumsLooselyEquals(point.p.y, arc[1].y)));
+ });
+
+ resultOrderedPoints = [{p: arc[0], angle: 0}, ...resultOrderedPoints, {p: arc[1], angle: Math.atan2(arc[1].y - arc[2].y, arc[1].x - arc[2].x)}];
+
+ let segments: [Coord, Coord, Coord | null, Link][] = [];
+ for (let i = 0; i < resultOrderedPoints.length - 1; i++) {
+ const pointOne = resultOrderedPoints[i];
+ const pointTwo = resultOrderedPoints[i + 1];
+ segments.push([pointOne.p, pointTwo.p, arc[2], arc[3]]);
+ }
+ return segments;
+ } else {
+ return [];
+ }
+ }
+
+ // determines whether two lines have the same contents, so they are equal
+ equalLines(line1: [Coord, Coord, Coord | null, Link], line2: [Coord, Coord, Coord | null, Link]): boolean {
+ if (
+ line1[0].equals(line2[0], 1) &&
+ line1[1].equals(line2[1], 1)
+ ) {
+ if (line1[2] === null && line2[2] === null) {
+ return true;
+ } else if (line1[2] !== null &&
+ line2[2] !== null &&
+ line1[2].equals(line2[2], 1)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // checks if point is on the shape outline
+ isPointOnLink(point: Coord, externalLines:[Coord, Coord, Coord | null, Link][], radius: number): boolean {
+ let pointOnLink = false;
+ externalLines.forEach((line) => {
+ let isSegmentOnLine: boolean;
+ if (line[2] !== null) {
+ isSegmentOnLine = this.isPointInArc(point, line[0], line[1], line[2], radius);
+ } else {
+ isSegmentOnLine = this.isPointOnLine(point, line[0], line[1]);
+ }
+ if (isSegmentOnLine) {
+ pointOnLink = true;
+ }
+ })
+ return pointOnLink;
+ }
+
+ isPointInsideLink(startPosition: Coord, startLink: Link, externalLines:[Coord, Coord, Coord | null, Link][], radius: number): boolean {
+ // check if the point is inside the shape created by the lines
+ // draw a line that is infinitely long and check if it intersects with the shape an odd number of times
+ const infiniteLine: [Coord, Coord, Coord | null, Link] = [startPosition, new Coord(startPosition.x + 10000, startPosition.y), null, startLink];
+
+ let intersections = 0;
+ externalLines.forEach((line) => {
+ const intersectionPoints = this.intersectsWith(infiniteLine, line, radius);
+
+ if (intersectionPoints !== undefined) {
+ // check for duplicate intersection points
+ let duplicatePoints: number[] = [];
+ intersectionPoints.forEach((point, index) => {
+ for (let j = index + 1; j < intersectionPoints.length; j++) {
+ if (this.twoNumsLooselyEquals(point.x, intersectionPoints[j].x) && this.twoNumsLooselyEquals(point.y, intersectionPoints[j].y)) {
+ duplicatePoints.push(index);
+ break;
+ }
+ }
+ });
+
+ let removedDuplicateIntersectionPoints: Coord[] = [];
+ intersectionPoints.forEach((point, i) => {
+ if (!duplicatePoints.includes(i)) {
+ removedDuplicateIntersectionPoints.push(point);
+ }
+ });
+
+ // check if the line from the link is an arc and the intersection is perpendicular (tangent)
+ let tangentIntersectionPoints: number[] = [];
+ removedDuplicateIntersectionPoints.forEach((point, index) => {
+ let infiniteLineSlope: number = (point.y - startPosition.y) / (point.x - startPosition.x);
+ let arcSlope: number = 0;
+ if (line[2] !== null) {
+ arcSlope = (point.y - line[2].y) / (point.x - line[2].x);
+ }
+ if (this.twoNumsLooselyEquals(arcSlope * infiniteLineSlope, -1)) {
+ tangentIntersectionPoints.push(index);
+ } else if (arcSlope === 0 && (infiniteLineSlope === Number.NEGATIVE_INFINITY || infiniteLineSlope === Number.POSITIVE_INFINITY)) {
+ tangentIntersectionPoints.push(index);
+ } else if ((arcSlope === Number.NEGATIVE_INFINITY || arcSlope === Number.POSITIVE_INFINITY) && infiniteLineSlope === 0) {
+ tangentIntersectionPoints.push(index);
+ }
+ });
+
+ let finalIntersectionPoints: {x: number, y: number}[] = [];
+ removedDuplicateIntersectionPoints.forEach((point, i) => {
+ if (!tangentIntersectionPoints.includes(i)) {
+ finalIntersectionPoints.push(point);
+ }
+ });
+
+ intersections += finalIntersectionPoints.length;
+ }
+
+ });
+
+ // If the number of intersections is odd, then the point is inside the shape
+ return intersections % 2 === 1;
+ }
+
+ // calculates whether a line is contained within a link
+ // used to determine whether to get rid of a line
+ isLineContained(line: [Coord, Coord, Coord | null, Link], linkExternalLines: [Coord, Coord, Coord | null, Link][], radius: number, intersectionPoints: {point: Coord, i: number}[]): boolean {
+ const origStartOnLink: boolean = this.isPointOnLink(line[0], linkExternalLines, radius);
+ const origEndOnLink: boolean = this.isPointOnLink(line[1], linkExternalLines, radius);
+ if (origStartOnLink && origEndOnLink) {
+ let tempShortenedLine:[Coord, Coord, Coord | null, Link] = this.shortenBy(line, 0.02 * this.scale);
+
+ const startOnLink: boolean = this.isPointOnLink(tempShortenedLine[0], linkExternalLines, radius);
+ const endOnLink: boolean = this.isPointOnLink(tempShortenedLine[1], linkExternalLines, radius);
+
+ return (this.isPointInsideLink(tempShortenedLine[0], tempShortenedLine[3], linkExternalLines, radius) || startOnLink) &&
+ (this.isPointInsideLink(tempShortenedLine[1], tempShortenedLine[3], linkExternalLines, radius) || endOnLink);
+ } else {
+ return (this.isPointInsideLink(line[0], line[3], linkExternalLines, radius) || origStartOnLink) &&
+ (this.isPointInsideLink(line[1], line[3], linkExternalLines, radius) || origEndOnLink);
+ }
+ }
+
+ // this shortens segments by shortenNum and returns new segment
+ shortenBy(line: [Coord, Coord, Coord | null, Link], shortenNum: number): [Coord, Coord, Coord | null, Link] {
+ let shortenedLine:[Coord, Coord, Coord | null, Link];
+ if (line[2] == null) {
+ shortenedLine = [line[0].clone(), line[1].clone(), line[2], line[3]];
+ const angle = Math.atan2(line[1].y - line[0].y, line[1].x - line[0].x);
+ const shortenByVector = new Coord(Math.cos(angle), Math.sin(angle)).scale(
+ shortenNum / 2
+ );
+ shortenedLine[0] = shortenedLine[0].add(shortenByVector);
+ shortenedLine[1] = shortenedLine[1].subtract(shortenByVector);
+ return shortenedLine;
+ } else {
+ shortenedLine = [line[0].clone(), line[1].clone(), line[2]?.clone(), line[3]];
+
+ let radius = line[0].getDistanceTo(line[2]);
+ let angleToShortenBy = shortenNum / radius;
+ angleToShortenBy /= 2;
+
+ let startAngle = line[0].getAngleTo(line[2]);
+ let endAngle = line[1].getAngleTo(line[2]);
+
+ startAngle += angleToShortenBy;
+ endAngle -= angleToShortenBy;
+
+ shortenedLine[0] = line[2].clone()?.add(
+ new Coord(Math.cos(startAngle), Math.sin(startAngle)).scale(radius)
+ );
+ shortenedLine[1] = line[2].clone()?.add(
+ new Coord(Math.cos(endAngle), Math.sin(endAngle)).scale(radius)
+ );
+
+ return shortenedLine;
+ }
+ }
+
+ // This function returns the lines used to calculate and save the lines of a link
+ solveForExternalLines(subLinks: Map, radius: number): [Coord, Coord, Coord | null, Link][] {
+ // create a list of all the external lines of all the sublinks
+ /* First Coord is starting position
+ Second Coord is ending position
+ boolean is true if the line is an arc
+ Last is the center Coord of an arc, null if it is just a line
+ */
+ let externalLines: [Coord, Coord, Coord | null, Link][] = [];
+ let allLinkExternalLines: Map = new Map();
+
+ // find and add all external lines for sublinks
+ for (let link of subLinks.values()) {
+ let allCoords: Coord[] = [];
+ for(let joint of link.joints.values()) {
+ allCoords.push(joint._coords);
+ }
+ allCoords = Array.from(allCoords, (coord) => {
+ return this.unitConversionService.modelCoordToSVGCoord(coord);
+ });
+
+ let linkExternalLines: [Coord, Coord, Coord | null, Link][] = [];
+
+ // find the hull points using the coords
+ let hullPoints: Coord[] = this.grahamScan(allCoords);
+ let collinearCoords: Coord[] | undefined = this.findCollinearCoords(allCoords);
+
+ if (collinearCoords !== undefined) {
+ // Calculate perpendicular direction vectors for the line
+ const dirFirstToSecond = this.perpendicularDirection(collinearCoords[0], collinearCoords[1]);
+ const dirSecondToFirst = this.perpendicularDirection(collinearCoords[1], collinearCoords[0]);
+
+ // Line from first to second Point
+ const point1: Coord = new Coord(collinearCoords[0].x + dirFirstToSecond.x * radius, collinearCoords[0].y + dirFirstToSecond.y * radius);
+ const point2: Coord = new Coord(collinearCoords[1].x + dirFirstToSecond.x * radius, collinearCoords[1].y + dirFirstToSecond.y * radius);
+ externalLines.push([point1.clone(), point2.clone(), null, link]);
+ linkExternalLines.push([point1.clone(), point2.clone(), null, link]);
+
+ // Arc around first joint
+ const point3: Coord = new Coord(collinearCoords[1].x + dirSecondToFirst.x * radius, collinearCoords[1].y + dirSecondToFirst.y * radius);
+ externalLines.push([point2.clone(), point3.clone(), collinearCoords[1].clone(), link]);
+ linkExternalLines.push([point2.clone(), point3.clone(), collinearCoords[1].clone(), link]);
+
+ // Line from second back to first Point
+ const point4: Coord = new Coord(collinearCoords[0].x + dirSecondToFirst.x * radius, collinearCoords[0].y + dirSecondToFirst.y * radius);
+ externalLines.push([point3.clone(), point4.clone(), null, link]);
+ linkExternalLines.push([point3.clone(), point4.clone(), null, link]);
+
+ // Arc around the second joint
+ const point5: Coord = new Coord(collinearCoords[0].x + dirFirstToSecond.x * radius, collinearCoords[0].y + dirFirstToSecond.y * radius);
+ externalLines.push([point4.clone(), point5.clone(), collinearCoords[0].clone(), link]);
+ linkExternalLines.push([point4.clone(), point5.clone(), collinearCoords[0].clone(), link]);
+ } else {
+ if (hullPoints.length < 3) {
+ throw new Error('At least three points are required to create a path with rounded corners.');
+ }
+
+ // console.log("hull points");
+ // console.log(hullPoints);
+
+ // Start the path, moving the pointer to the first correct point
+ const dirFirstToSecondInit = this.perpendicularDirection(hullPoints[0], hullPoints[1]);
+
+ // last position
+ let lastPosition: Coord = new Coord(hullPoints[0].x + dirFirstToSecondInit.x * radius, hullPoints[0].y + dirFirstToSecondInit.y * radius);
+
+ //iterate over all the points, one line and one arc at a time
+ for (let i = 0; i < hullPoints.length; i++) {
+ //get current points to look at.
+ const c0 = hullPoints[i];
+ const c1 = hullPoints[(i + 1) % hullPoints.length];
+ const c2 = hullPoints[(i + 2) % hullPoints.length];
+ //get the Perpendicular Direction for the Path
+ const dirFirstToSecond = this.perpendicularDirection(c0, c1);
+ const dirSecondToThird = this.perpendicularDirection(c1, c2);
+
+ // Line from first joint to second joint
+ let point1: Coord = new Coord(c1.x + dirFirstToSecond.x * radius, c1.y + dirFirstToSecond.y * radius);
+ externalLines.push([lastPosition, point1, null, link]);
+ linkExternalLines.push([lastPosition, point1, null, link]);
+
+ // Arc around second joint
+ let point2: Coord = new Coord(c1.x+ dirSecondToThird.x * radius, c1.y + dirSecondToThird.y * radius);
+ let point2Center: Coord = c1.clone();
+ externalLines.push([point1, point2, point2Center, link]);
+ linkExternalLines.push([point1, point2, point2Center, link]);
+ lastPosition = point2.clone();
+ }
+ }
+
+ allLinkExternalLines.set(link, linkExternalLines);
+ }
+
+ // need to process the external lines we got
+ let stopCounter = 10000;
+
+ // will hold the intersection points of all external lines
+ // x is the x coord of intersection, y is y coord of intersection, and i is index in external lines array
+ let allIntersectionPoints: {point: Coord, i: number}[] = [];
+
+ // for each external line, need to check for intersections with all other external lines
+ for (let i = 0; i < externalLines.length - 1; i++) {
+ const line1: [Coord, Coord, Coord | null, Link] = externalLines[i];
+ for (let j = i + 1; j < externalLines.length; j++) {
+ if (stopCounter-- < 0) {
+ console.error("Error, stop counter is at 0");
+ return [];
+ }
+
+ const line2: [Coord, Coord, Coord | null, Link] = externalLines[j];
+
+ // check if lines intersect, if they do save the intersections
+ const intersection = this.intersectsWith(line1, line2, radius);
+
+ // make sure intersection is not the end points of a segment before adding in the intersection for each segment
+ // intersection points are saved along with the segment the intersection is on
+ if (intersection !== undefined) {
+ intersection.forEach((point: Coord) => {
+ if (!(line1[1].equals(point, this.scale) || line1[0].equals(point, this.scale))) {
+ allIntersectionPoints.push({point: point, i: i});
+ }
+ if (!(line2[0].equals(point, this.scale) || line2[1].equals(point, this.scale))) {
+ allIntersectionPoints.push({point: point, i: j});
+ }
+ });
+ }
+
+ }
+ }
+
+ // checking if there are any duplicate intersection points for each line
+ let duplicatePoints: number[] = [];
+ allIntersectionPoints.forEach((point, index) => {
+ for (let j = index + 1; j < allIntersectionPoints.length; j++) {
+ if (this.twoNumsLooselyEquals(point.point.x, allIntersectionPoints[j].point.x) && this.twoNumsLooselyEquals(point.point.y, allIntersectionPoints[j].point.y) && point.i === allIntersectionPoints[j].i) {
+ duplicatePoints.push(index);
+ break;
+ }
+ }
+ });
+
+ // removing the duplicate intersection points, if any
+ let removedDuplicatePoints: {point: Coord, i: number}[] = [];
+ allIntersectionPoints.forEach((point, index) => {
+ if (!duplicatePoints.includes(index)) {
+ removedDuplicatePoints.push(point);
+ }
+ });
+
+ // grouping intersections by line
+ let intersectionsByCoords: Coord[][] = this.groupIntersectionsByPath(externalLines, removedDuplicatePoints);
+
+ // will hold the new external lines with the intersections considered
+ let intersectionExternalLines: [Coord, Coord, Coord | null, Link][] = [];
+
+ // splitting each line by intersections
+ for (let i = 0; i < externalLines.length; i++) {
+ if (externalLines[i][2] === null) {
+ // segment is a line
+ let lineSegments = this.splitLineByIntersections(externalLines[i], intersectionsByCoords[i]);
+ intersectionExternalLines.push(...lineSegments);
+ } else {
+ // segment is an arc
+ let arcSegments = this.splitArcByIntersection(externalLines[i], intersectionsByCoords[i]);
+ intersectionExternalLines.push(...arcSegments);
+ }
+ }
+
+ // check for duplicate lines
+ let duplicateLines: [Coord, Coord, Coord | null, Link][] = [];
+ for (const line of intersectionExternalLines) {
+ const duplicateFound = intersectionExternalLines.find(line2 => {
+ return line !== line2 && this.equalLines(line, line2);
+ });
+ const inDuplicateLines = duplicateLines.find((line2) => {
+ return this.equalLines(line2, line);
+ });
+ if (duplicateFound && !inDuplicateLines) {
+ duplicateLines.push(line);
+ }
+ }
+
+ // if a segment is fully inside another link, remove it
+ intersectionExternalLines = intersectionExternalLines.filter((line) => {
+ return ![... subLinks.values()].some((link) => {
+ if (link === line[3]) {
+ return false;
+ } else {
+ let linkLines = allLinkExternalLines.get(link);
+ if (linkLines) {
+ return this.isLineContained(line, linkLines, radius, allIntersectionPoints);
+ } else {
+ return false;
+ }
+ }
+ })
+ });
+
+ // duplicate lines are only added if we detect a gap in the path
+ // need to loop again and check for a gap if a new segment was added
+ let addedNewLines: boolean = false;
+ let firstTime: boolean = true;
+ let linesToCheck: [Coord, Coord, Coord | null, Link][] = intersectionExternalLines;
+ let newLinesToAdd: [Coord, Coord, Coord | null, Link][];
+ while (firstTime || addedNewLines) {
+ firstTime = false;
+ addedNewLines = false;
+
+ newLinesToAdd = [];
+ for (let i = 0; i < linesToCheck.length; i++) {
+ const pointToSearch: Coord = linesToCheck[i][1];
+ const found = intersectionExternalLines.find((line2) => {
+ return line2[0].equals(pointToSearch, 1);
+ });
+ if (!found) {
+ const lineToAdd = duplicateLines.find((line2) => {
+ return line2[0].looselyEquals(pointToSearch, 1);
+ });
+ if (lineToAdd !== undefined) {
+ newLinesToAdd.push(lineToAdd);
+ addedNewLines = true;
+ }
+ }
+ }
+ linesToCheck = newLinesToAdd;
+
+ // add in the new lines
+ intersectionExternalLines.push(...newLinesToAdd);
+ }
+
+ //Remove very short lines
+ intersectionExternalLines = intersectionExternalLines.filter((line) => {
+ return (line[0].getDistanceTo(line[1]) > 0.00001 * 1);
+ });
+
+ return intersectionExternalLines;
+ }
+
+ calculateCompoundPath(subLinks: Map, allCoordsList: Coord[], r: number) {
+ // allExternalLines will hold the path coordinates for each line and arc for all the coords
+ let allExternalLines: [Coord, Coord, Coord | null, Link][] = this.solveForExternalLines(subLinks, r);
+ //console.log(allExternalLines);
+
+ if (allExternalLines.length < 1) {
+ return '';
+ }
+
+ // converting external lines list into a set, which we will delete lines we have used
+ const externalLinesSet = new Set(allExternalLines);
+
+ let pathString = '';
+
+ let timeoutCounter = 1000;
+
+ // this is the offset for concave curve radius
+ const offsetRadius = 3;
+
+ while (externalLinesSet.size > 1) {
+ // this is the first line from the set
+ let currentLine: [Coord, Coord, Coord | null, Link] = externalLinesSet.values().next().value;
+
+ let beginningPoint: Coord = currentLine[1].clone();
+
+ while (!currentLine[1].equals(beginningPoint, 1) || this.isNewShape(pathString)) {
+ if (timeoutCounter-- < 0) {
+ console.log("Timeout counter reached!")
+ return pathString;
+ }
+
+ // find the next line that starts at the end of the current line
+ const nextLine: [Coord, Coord, Coord | null, Link] | undefined = [...externalLinesSet].find((line) => {
+ return line[0].looselyEquals(currentLine[1], 1);
+ });
+
+ if (!nextLine) {
+ break;
+ }
+ externalLinesSet.delete(nextLine);
+
+ // when there are two lines intersecting, create a fillet between them
+ if (currentLine[2] === null && nextLine[2] === null &&
+ // checking if angle between the two lines is greater than 10 degrees
+ Math.abs(this.angleToPI(nextLine, currentLine)) > (10 * Math.PI / 180)) {
+ let [currentLineOffsetPoint, nextLineOffsetPoint, radius] = this.computeArcPointsAndRadius(currentLine, nextLine, r * offsetRadius);
+
+ currentLine[1] = currentLineOffsetPoint;
+ nextLine[0] = nextLineOffsetPoint;
+
+ if (this.isNewShape(pathString)) {
+ pathString += 'M ' + currentLine[1].x + ', ' + currentLine[1].y + ' ';
+ } else {
+ pathString = this.pathStringForLine(currentLine, pathString);
+ }
+
+ pathString += 'A ' + radius + ', ' + radius + ' 0 0 0 ' + nextLine[0].x + ', ' + nextLine[0].y + ' ';
+ } else {
+
+ // otherwise, just draw a line between the two points
+ if (this.isNewShape(pathString)) {
+ pathString += 'M ' + currentLine[1].x + ', ' + currentLine[1].y + ' ';
+ } else {
+ pathString = this.pathStringForLine(currentLine, pathString);
+ }
+ }
+ currentLine = nextLine;
+ }
+ pathString = this.pathStringForLine(currentLine, pathString);
+ pathString += 'Z ';
+ }
+ return pathString;
+ }
+
+ // calculate and return the path string plus the path of the line
+ pathStringForLine(line: [Coord, Coord, Coord | null, Link], pathString: string):string {
+ if (line[2] !== null) {
+ // if the line is an arc
+ pathString += 'A ' + line[0].getDistanceTo(line[2]) + ', ' + line[0].getDistanceTo(line[2]) + ' 0 0 1 ' + line[1].x + ', ' + line[1].y + ' ';
+ } else {
+ // if the line is just a line
+ pathString += 'L ' + line[1].x + ', ' + line[1].y + ' ';
+ }
+ return pathString;
+ }
+
+
}
diff --git a/src/app/services/undo-redo.service.ts b/src/app/services/undo-redo.service.ts
index 5bdce848..23957651 100644
--- a/src/app/services/undo-redo.service.ts
+++ b/src/app/services/undo-redo.service.ts
@@ -1,14 +1,15 @@
-import {Action} from '../components/ToolBar/undo-redo-panel/action';
-import {Coord} from '../model/coord';
-import {Link} from '../model/link';
-import {Joint} from '../model/joint';
-import {Mechanism} from '../model/mechanism';
-import {Subject} from 'rxjs';
-import {Injectable} from '@angular/core';
-import {StateService} from './state.service';
-import {isUndefined} from 'lodash';
-import {Position} from '../model/position';
-import {Force} from '../model/force';
+import { Action } from '../components/ToolBar/undo-redo-panel/action';
+import { Coord } from '../model/coord';
+import { Link } from '../model/link';
+import { Joint } from '../model/joint';
+import { Mechanism } from '../model/mechanism';
+import { Subject } from 'rxjs';
+import { Injectable } from '@angular/core';
+import { StateService } from './state.service';
+import { isUndefined } from 'lodash';
+import { Position } from '../model/position';
+import { Force } from '../model/force';
+import {CompoundLink} from "../model/compound-link";
@Injectable({
providedIn: 'root',
@@ -262,7 +263,8 @@ export class UndoRedoService {
const tr = action.linkTracerData!;
this.mechanism.addJointToLink(
tr.linkId,
- new Coord(tr.coords.x, tr.coords.y)
+ new Coord(tr.coords.x, tr.coords.y),
+ true
);
break;
case 'addLinkToLink':
@@ -299,6 +301,24 @@ export class UndoRedoService {
//reverses the lock
link.locked = !locked
break;
+ } case 'circleLink': {
+ //get the specified link from the action and see if it's a circle
+ const link = this.mechanism.getLink(action.linkId!)!;
+ const circular = link.isCircle;
+ //reverse the lock
+ link.isCircle = !circular
+ break;
+ } case 'deleteCompoundLink': {
+ this.mechanism.removeCompoundLinkByID(action.compoundLinkData!.id);
+ break;
+ } case 'addTracerCompound': {
+ const tr = action.linkTracerData!;
+ this.mechanism.addTracerPointWelded(
+ tr.linkId,
+ tr.tracerModelPos!,
+ tr.tracerSVGPos!,
+ );
+ break;
}
default:
@@ -312,11 +332,6 @@ export class UndoRedoService {
// Applies the inverse of an action to the Mechanism (undo logic).
private applyInverseAction(action: Action): void {
switch (action.type) {
- case 'addWeld':
- if (action.jointId !== undefined) {
- this.mechanism.removeWeld(action.jointId);
- }
- break;
case 'addInput':
if (action.jointId !== undefined) {
this.mechanism.removeInput(action.jointId);
@@ -327,14 +342,14 @@ export class UndoRedoService {
this.mechanism.addInput(action.jointId);
}
break;
- case 'removeSlider':
+ case 'addGround':
if (action.jointId !== undefined) {
- this.mechanism.addSlider(action.jointId);
+ this.mechanism.removeGround(action.jointId);
}
break;
- case 'removeWeld':
+ case 'removeGround':
if (action.jointId !== undefined) {
- this.mechanism.addWeld(action.jointId);
+ this.mechanism.addGround(action.jointId);
}
break;
case 'generateFourBar':
@@ -343,6 +358,11 @@ export class UndoRedoService {
case 'generateSixBar':
this.generateSixBarSubject.next();
break;
+ case 'addSlider':
+ if (action.jointId !== undefined) {
+ this.mechanism.removeSlider(action.jointId);
+ }
+ break;
case 'setSynthesisLength':
this.mechanism.setCouplerLength(action.oldDistance as number);
break;
@@ -379,9 +399,21 @@ export class UndoRedoService {
this.mechanism.setPositionAngle(action.oldAngle, action.linkId);
}
break;
- case 'addSlider': // Restores the link/joints from snapshot if user
- case 'addGround': // performs an undo on add slider, add ground,
- case 'removeGround': // remove ground, or delete joint
+ case 'removeSlider':
+ if (action.jointId !== undefined) {
+ this.mechanism.addSlider(action.jointId);
+ }
+ break;
+ case 'addWeld':
+ if (action.jointId !== undefined) {
+ this.mechanism.removeWeld(action.jointId);
+ }
+ break;
+ case 'removeWeld':
+ if (action.jointId !== undefined) {
+ this.mechanism.addWeld(action.jointId);
+ }
+ break;
case 'deleteJoint':
// Restore main joint:
if (action.jointData) {
@@ -410,6 +442,22 @@ export class UndoRedoService {
}
});
}
+
+ if (action.compoundLinkDataArray) {
+ action.compoundLinkDataArray.forEach((cLinkSnap) => {
+ const cLinks = cLinkSnap.linkIds.map((lid) =>
+ this.mechanism.getLink(lid)
+ );
+ if (cLinks.every((l) => l !== undefined)) {
+ const restoredCompoundLink = new CompoundLink(cLinkSnap.id, cLinks);
+ restoredCompoundLink.name = cLinkSnap.name;
+ restoredCompoundLink.mass = cLinkSnap.mass;
+ restoredCompoundLink.lock = cLinkSnap.locked;
+ restoredCompoundLink.color = cLinkSnap.color;
+ this.mechanism._addCompoundLink(restoredCompoundLink);
+ }
+ });
+ }
break;
case 'moveJoint':
if (action.jointId != null && action.oldCoords) {
@@ -530,6 +578,102 @@ export class UndoRedoService {
link.locked = !locked
break;
}
+ case 'circleLink': {
+ //get the specified link from the action and see if it's a circle
+ const link = this.mechanism.getLink(action.linkId!)!;
+ const circular = link.isCircle
+ //reverse the lock
+ link.isCircle = !circular
+ break;
+ }
+ case 'deleteCompoundLink': {
+ // rebuilding joints
+ if (action.compoundExtraJointsData) {
+ action.compoundExtraJointsData!.forEach((joints) => {
+ joints.forEach((j) => {
+ if (!this.mechanism.getJoint(j.id)) {
+ this.restoreJointFromSnapshot(j);
+ }
+ })
+ })
+ }
+
+ // rebuilding links
+ if (action.compoundExtraLinkData) {
+ action.compoundExtraLinkData!.forEach((link) => {
+ // check that joints exist
+ const jointObjs = link.jointIds.map((id) => {
+ const j = this.mechanism.getJoint(id);
+ if (!j) console.warn(`undo deleteLink: joint ${id} still missing`);
+ return j!;
+ });
+
+ if (jointObjs.every((j) => j != null)) {
+ const linkRestored = new Link(link.id, jointObjs);
+ linkRestored.name = link.name;
+ linkRestored.mass = link.mass;
+ linkRestored.angle = link.angle;
+ linkRestored.locked = link.locked;
+ linkRestored.color = link.color;
+ this.mechanism._addLink(linkRestored);
+ }
+ })
+ }
+
+ // rebuilding compound link
+ const cl = action.compoundLinkData!;
+ const linkObjs = cl.linkIds.map((id) => {
+ const l = this.mechanism.getLink(id);
+ if (!l) console.warn(`undo deleteCompoundLink: link ${id} still missing`);
+ return l!;
+ });
+ if (linkObjs.every((l) => l != null)) {
+ const linkRestored = new CompoundLink(cl.id, linkObjs);
+ linkRestored.name = cl.name;
+ linkRestored.mass = cl.mass;
+ linkRestored.lock = cl.locked;
+ linkRestored.color = cl.color;
+ this.mechanism._addCompoundLink(linkRestored);
+ }
+
+ break;
+ }
+ case 'addTracerCompound': {
+ const tr = action.linkTracerData!;
+ const allCompoundLinks = this.mechanism.getCompoundLinks();
+ let linkObj;
+ for (const c of allCompoundLinks) {
+ if (c.id == tr.linkId) {
+ linkObj = c;
+ }
+ }
+
+ if (linkObj == undefined) {
+ console.warn(
+ `Undo addTracerCompound: no compound link found)`
+ );
+ break;
+ }
+
+ let allJoints: Joint[] = [];
+ Array.from(linkObj!.links!.values()).forEach((l) => {
+ Array.from(l.joints.values()).forEach((j) => {
+ allJoints.push(j);
+ })
+ });
+
+ const toRemove = allJoints.find(
+ (j) => j.coords.x === tr.coords.x && j.coords.y === tr.coords.y
+ );
+ if (toRemove) {
+ this.mechanism.removeJoint(toRemove.id);
+ } else {
+ console.warn(
+ `Undo addTracerCompound: no tracer found at (${tr.coords.x},${tr.coords.y})`
+ );
+ }
+ break;
+ }
default:
console.error('No inverse defined for action type:', action.type);
}
@@ -552,6 +696,8 @@ export class UndoRedoService {
restoredJoint.locked = jointSnapshot!.locked;
restoredJoint.hidden = jointSnapshot!.isHidden;
restoredJoint.reference = jointSnapshot!.isReference;
+ restoredJoint.isTracer = jointSnapshot!.isTracer;
+ restoredJoint.isPartOfWelded = jointSnapshot!.isPartOfWelded;
this.mechanism._addJoint(restoredJoint);
}
}
diff --git a/src/app/services/unit-conversion.service.ts b/src/app/services/unit-conversion.service.ts
index cd3290e0..19cca1c8 100644
--- a/src/app/services/unit-conversion.service.ts
+++ b/src/app/services/unit-conversion.service.ts
@@ -31,4 +31,10 @@ export class UnitConversionService{
return new Coord(convertedX,convertedY);
}
+ // Converts model number to SVG number by applying the defined scale factor.
+ public modelNumToSVGNum(modelNum: number): number{
+ let convertedNum: number = modelNum * this.MODEL_TO_SVG_SCALE;
+ return convertedNum;
+ }
+
}
diff --git a/src/assets/contextMenuIcons/circleInputLink.svg b/src/assets/contextMenuIcons/circleInputLink.svg
new file mode 100644
index 00000000..f9c74c99
--- /dev/null
+++ b/src/assets/contextMenuIcons/circleInputLink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/contextMenuIcons/uncircular.svg b/src/assets/contextMenuIcons/uncircular.svg
new file mode 100644
index 00000000..00f29921
--- /dev/null
+++ b/src/assets/contextMenuIcons/uncircular.svg
@@ -0,0 +1 @@
+
\ No newline at end of file