Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added drawing functionality to tactical maps #3363

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
98fd3a2
added drawing functionality to tactical maps
May 19, 2023
b5b8683
removed redundant check
May 19, 2023
f458c8f
linter
May 19, 2023
eaeca24
more linter
May 19, 2023
be6d103
l-i-n-t-e-r
May 19, 2023
2a18885
prettier format
May 19, 2023
8cdfbeb
Merge branch 'master' into tacmap-refactor
Jun 28, 2023
c102fdc
progress
Jun 28, 2023
07d60ca
progress
Jun 30, 2023
a4cc32f
spaghet
Jun 30, 2023
a2de250
guh
Jun 30, 2023
9dd32dc
hacky, fix later.
Jun 30, 2023
8ed8d6d
fix
Jun 30, 2023
87d1c5b
Merge branch 'master' into tacmap-refactor
Jun 30, 2023
28e9a67
flaticon
Jul 1, 2023
108acb6
Merge branch 'tacmap-refactor' of github.com:Cthulhu80/cmss13 into ta…
Jul 1, 2023
03951dc
more more
Jul 1, 2023
6d6fe45
fix
Jul 1, 2023
efeabb8
bug fix
Jul 1, 2023
13037ba
fix
Jul 1, 2023
5ef3526
prettier
Jul 2, 2023
3d22bbd
fix
Jul 2, 2023
362df62
LINTER
Jul 2, 2023
d87da9e
fix
Jul 2, 2023
cf1cc1c
...
Jul 2, 2023
bdfadb9
fix
Jul 2, 2023
e21b083
Merge branch 'master' into tacmap-refactor
Jul 2, 2023
4cf8abf
typo
Jul 3, 2023
5d8784f
var refactor
Jul 3, 2023
3781a37
less shit code
Jul 4, 2023
22dbf83
fix
Jul 4, 2023
9757437
too much python lol
Jul 4, 2023
396ce26
fox
Jul 4, 2023
13151cb
fix
Jul 4, 2023
dcd697c
fix
Jul 17, 2023
be1cef3
hacky svg implementation
Jul 17, 2023
f923e09
fix
Jul 17, 2023
945df1c
fix
Jul 17, 2023
8c59c07
Merge branch 'tacmap-refactor' of github.com:Cthulhu80/cmss13 into ta…
Jul 17, 2023
97c7616
fuck yeah
Jul 17, 2023
4b27583
svg
Jul 17, 2023
18f1080
prettier
Jul 18, 2023
f54cb6e
Merge branch 'master' into tacmap-refactor
Jul 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions code/_globalvars/global_lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ GLOBAL_LIST_EMPTY(CMBFaxes)
GLOBAL_LIST_EMPTY(GeneralFaxes) //Inter-machine faxes
GLOBAL_LIST_EMPTY(fax_contents) //List of fax contents to maintain it even if source paper is deleted

GLOBAL_LIST_EMPTY(canvas_drawings) //List of canvas drawings

GLOBAL_LIST_EMPTY(failed_fultons) //A list of fultoned items which weren't collected and fell back down
GLOBAL_LIST_EMPTY(larva_burst_by_hive)

Expand Down
103 changes: 101 additions & 2 deletions code/controllers/subsystem/minimap.dm
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,21 @@ SUBSYSTEM_DEF(minimaps)
var/targeted_ztrait = ZTRAIT_GROUND
var/atom/owner

// used in cic tactical maps for drawing on the canvas, defaults to blue.
var/toolbar_color_selection = "black"

// hacky ass solution for tgui color display, move functionality to js, fix later.
var/toolbar_updated_selection = "black"

var/canvas_cooldown_time = 4 MINUTES

// prevent multiple users from updating the canvas image until the cooldown expires.
COOLDOWN_DECLARE(canvas_cooldown)

var/updated_canvas = FALSE

var/datum/tacmap_holder/map_holder
var/img_ref

/datum/tacmap/New(atom/source, minimap_type)
allowed_flags = minimap_type
Expand All @@ -472,15 +486,93 @@ SUBSYSTEM_DEF(minimaps)

ui = SStgui.try_update_ui(user, src, ui)
if(!ui)

var/icon/map_icon = getFlatIcon(map_holder.map)
img_ref = icon2html(map_icon, user, sourceonly = TRUE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this cause a performance issue if say, suddenly, 30-40 marines all request the image at once? I can't remember if repeated calls will recache it.

Copy link
Author

@ghost ghost Jul 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my initial commit lul, why ever did I delete this comment
Screen Shot 2023-07-04 at 12 43 16 AM


user.client.register_map_obj(map_holder.map)
ui = new(user, src, "TacticalMap")
ui.open()

/datum/tacmap/ui_static_data(mob/user)

/datum/tacmap/ui_data(mob/user)
var/list/data = list()
data["mapRef"] = map_holder.map_ref
data["imageSrc"] = img_ref
data["toolbarColorSelection"] = toolbar_color_selection
data["toolbarUpdatedSelection"] = toolbar_updated_selection
data["worldtime"] = world.time

data["canvasCooldown"] = canvas_cooldown
data["nextCanvasTime"] = canvas_cooldown_time
data["updatedCanvas"] = updated_canvas
return data

/datum/tacmap/ui_close(mob/user)
. = ..()
updated_canvas = FALSE
toolbar_color_selection = "black"
toolbar_updated_selection = "black"

/datum/tacmap/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return

var/user = ui.user

switch (action)
if ("clearCanvas")
toolbar_updated_selection = "clear"
This conversation was marked as resolved.
Show resolved Hide resolved
. = TRUE

if ("updateCanvas")
if(alert(user, "Are you sure you want to update the canvas changes?", "Confirm?", "Yes", "No") == "No")
return

toolbar_updated_selection = "export"
COOLDOWN_START(src, canvas_cooldown, canvas_cooldown_time)
updated_canvas = TRUE
. = TRUE

if ("undoChange")
toolbar_updated_selection = "undo"
. = TRUE

if ("selectColor")

toolbar_color_selection = params["color"]
toolbar_updated_selection = toolbar_color_selection
. = TRUE

if ("selectAnnouncement")
toolbar_updated_selection = "export"

to_chat(usr, SPAN_WARNING(params["image"]))
var/datum/canvas_map/canvas_image = new(params["image"])

GLOB.canvas_drawings += canvas_image

var/input = stripped_multiline_input(user, "Optional message to announce to the [MAIN_SHIP_NAME]'s crew with the tactical map", "Tactical Map Announcement", "")
var/signed
if(ishuman(user))
var/mob/living/carbon/human/H = user
var/obj/item/card/id/id = H.wear_id
if(istype(id))
var/paygrade = get_paygrades(id.paygrade, FALSE, H.gender)
signed = "[paygrade] [id.registered_name]"

var/href_map = "<a href='?MapView=\ref[canvas_image]'>View Tactical Map</a><br><br>"
var/outgoing_message = href_map + input

message_admins("[key_name(user)] has made a tactical map announcement.")
log_announcement("[key_name(user)] has announced the following: [outgoing_message]")

//hardcoded testing, PROGRESS!
marine_announcement(outgoing_message, "Tactical Map Announcement", signature = signed)
updated_canvas = FALSE
qdel(canvas_image)
. = TRUE

/datum/tacmap/ui_status(mob/user)
if(!(isatom(owner)))
return UI_INTERACTIVE
Expand All @@ -506,3 +598,10 @@ SUBSYSTEM_DEF(minimaps)
/datum/tacmap_holder/Destroy()
map = null
return ..()

/datum/canvas_map
var/data

/datum/canvas_map/New(data)
. = ..()
src.data = data
9 changes: 9 additions & 0 deletions code/modules/client/client_procs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list(
cmd_admin_pm(receiver_client, null)
return

else if(href_list["MapView"])

var/datum/canvas_map/image = locate(href_list["MapView"])

if(!istype(image))
return

show_browser(usr, "<img src=[image.data] height=1000 width=1000>","Tactical Map", "Tactical Map", "size=1000x1000")

else if(href_list["FaxView"])

var/datum/fax/info = locate(href_list["FaxView"])
Expand Down
240 changes: 240 additions & 0 deletions tgui/packages/tgui/interfaces/CanvasLayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { Component, createRef } from 'inferno';

// this file should probably not be in interfaces, should move it later.
export class CanvasLayer extends Component {
constructor(props) {
super(props);
this.canvasRef = createRef();

// color selection
// using this.state prevents unpredictable behavior
this.state = {
selection: this.props.selection,
};

// needs to be of type png of jpg
this.img = null;
this.imageSrc = this.props.imageSrc;

// stores the stacked lines
this.lineStack = [];
This conversation was marked as resolved.
Show resolved Hide resolved

// stores the individual line drawn
this.currentLine = [];

this.ctx = null;
this.isPainting = false;
this.lastX = null;
this.lastY = null;
}

componentDidMount() {
this.ctx = this.canvasRef.current.getContext('2d');
this.ctx.lineWidth = 4;
this.ctx.lineCap = 'round';

this.img = new Image();

// hardcoded value for testing pngs
// this.img.src = "https://cm-ss13.com/wiki/images/6/6f/LV624.png"

this.img.src = this.imageSrc;

this.drawCanvas();

this.canvasRef.current.addEventListener('mousedown', this.handleMouseDown);
this.canvasRef.current.addEventListener('mousemove', this.handleMouseMove);
this.canvasRef.current.addEventListener('mouseup', this.handleMouseUp);
}

componentWillUnmount() {
this.canvasRef.current.removeEventListener(
'mousedown',
this.handleMouseDown
);
this.canvasRef.current.removeEventListener(
'mousemove',
this.handleMouseMove
);
this.canvasRef.current.removeEventListener('mouseup', this.handleMouseUp);
}

handleMouseDown = (e) => {
this.isPainting = true;

const rect = this.canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

this.ctx.beginPath();
this.ctx.moveTo(this.lastX, this.lastY);
this.lastX = x;
this.lastY = y;
};

handleMouseMove = (e) => {
if (!this.isPainting || !this.state.selection) {
return;
}

// defaults to black sometimes, it's a bug I think maybe.
this.ctx.strokeStyle = this.state.selection;

const rect = this.canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

if (this.lastX !== null && this.lastY !== null) {
this.ctx.moveTo(this.lastX, this.lastY);
this.ctx.lineTo(x, y);
this.ctx.stroke();
this.currentLine.push([
this.lastX,
this.lastY,
x,
y,
this.state.selection,
]);
}

this.lastX = x;
this.lastY = y;
};

handleMouseUp = () => {
this.isPainting = false;
this.lineStack.push([...this.currentLine]);
this.currentLine = [];
this.ctx.beginPath();
};

handleSelectionChange = () => {
const { selection } = this.props;

if (selection === 'clear') {
this.ctx.clearRect(
0,
0,
this.canvasRef.current.width,
this.canvasRef.current.height
);
this.ctx.drawImage(
this.img,
0,
0,
this.canvasRef.current.width,
this.canvasRef.current.height
);

this.lineStack = [];
return;
}

if (selection === 'undo') {
if (this.lineStack.length === 0) {
return;
}
const line = this.lineStack[this.lineStack.length - 1];

// selects last color before line is yeeted, this is buggy sometimes.
const prevColor = line[0][4];
this.lineStack.pop();
This conversation was marked as resolved.
Show resolved Hide resolved

this.ctx.clearRect(
0,
0,
this.canvasRef.current.width,
this.canvasRef.current.height
);
this.ctx.drawImage(
this.img,
0,
0,
this.canvasRef.current.width,
this.canvasRef.current.height
);
this.ctx.globalCompositeOperation = 'source-over';

this.lineStack.forEach((currentLine) => {
currentLine.forEach(([lastX, lastY, x, y, colorSelection]) => {
this.ctx.strokeStyle = colorSelection;
this.ctx.beginPath();
this.ctx.moveTo(lastX, lastY);
this.ctx.lineTo(x, y);
this.ctx.stroke();
});
});

this.setState({ selection: prevColor });
return;
}

if (selection === 'export') {
const svgData = this.convertToSVG();
this.props.onImageExport(svgData);
return;
}

this.setState({ selection: selection });
};

componentDidUpdate(prevProps) {
if (prevProps.selection !== this.props.selection) {
this.handleSelectionChange();
}
}

drawCanvas() {
this.img.onload = () => {
// this onload may or may not be causing problems.
this.ctx.drawImage(
this.img,
0,
0,
this.canvasRef.current.width,
this.canvasRef.current.height
);
};
}

convertToSVG() {
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttributeNS(null, 'width', this.canvasRef.current.width);
svg.setAttributeNS(null, 'height', this.canvasRef.current.height);

const lines = this.lineStack.flat();
lines.forEach(([lastX, lastY, x, y, colorSelection]) => {
const line = document.createElementNS(svgNS, 'line');
line.setAttributeNS(null, 'x1', lastX);
line.setAttributeNS(null, 'y1', lastY);
line.setAttributeNS(null, 'x2', x);
line.setAttributeNS(null, 'y2', y);
line.setAttributeNS(null, 'stroke', colorSelection);
line.setAttributeNS(null, 'stroke-width', '4');
line.setAttributeNS(null, 'stroke-linecap', 'round');
svg.appendChild(line);
});

const serializer = new XMLSerializer();
const serializedSvg = serializer.serializeToString(svg);
const base64Svg = btoa(serializedSvg);
const dataUrl = `data:image/svg+xml;base64,${base64Svg}`;

return dataUrl;
}

render() {
return (
<canvas
ref={this.canvasRef}
style={{
height: '100%', // causes discrepency between mouse and drawing line, fix later.
width: '100%',
}}
width={650}
height={590}
/>
);
}
}
Loading